Abilita la serializzazione HAL in Spring Boot per il metodo del controller personalizzato

Sto cercando di creare un’API RESTful con Spring Boot utilizzando spring-boot-starter-data-rest. Ci sono alcune entity framework: account, transazioni, categorie e utenti – solo le solite cose.

Quando recupero gli oggetti su http: // localhost: 8080 / transazioni tramite l’API che è stata generata per impostazione predefinita, tutto procede bene e ottengo un elenco con tutte le transazioni come oggetti JSON come quello:

{ "amount": -4.81, "date": "2014-06-17T21:18:00.000+0000", "description": "Pizza", "_links": { "self": { "href": "http://localhost:8080/transactions/5" }, "category": { "href": "http://localhost:8080/transactions/5/category" }, "account": { "href": "http://localhost:8080/transactions/5/account" } } } 

Ma ora l’objective è recuperare solo le ultime transazioni sotto quell’URL poiché non voglio serializzare l’intera tabella del database. Quindi ho scritto un controller:

 @Controller public class TransactionController { private final TransactionRepository transactionRepository; @Autowired public TransactionController(TransactionRepository transactionRepository) { this.transactionRepository = transactionRepository; } // return the 5 latest transactions @RequestMapping(value = "/transactions", method = RequestMethod.GET) public @ResponseBody List getLastTransactions() { return transactionRepository.findAll(new PageRequest(0, 5, new Sort(new Sort.Order(Sort.Direction.DESC, "date")))).getContent(); } } 

Quando ora provo ad accedere a http: // localhost: 8080 / transazioni c’è a

 java.lang.IllegalStateException: Cannot call sendError() after the response has been committed 

a causa del riferimento circolare tra utenti e account. Quando risolvo questo problema aggiungendo un’annotazione @JsonBackReference all’elenco degli account in Utente, posso recuperare l’elenco delle transazioni ma solo con questo formato “classico”:

 { "id": 5, "amount": -4.5, "date": "2014-06-17T21:18:00.000+0000", "description": "Pizza", "account": { "id": 2, "name": "Account Tilman", "owner": { "id": 1, "name": "Tilman" }, "categories": [ { "id": 1, "name": "Groceries" }, { "id": 2, "name": "Restaurant" } ], "users": [ { "id": 1, "name": "Tilman" } ] }, "category": { "id": 2, "name": "Restaurant" } } 

Non ci sono più collegamenti HAL, tutto viene serializzato direttamente da Jackson. Ho provato ad aggiungere

 @EnableHypermediaSupport(type = HypermediaType.HAL) 

alle classi di quadro ma questo non mi ha portato da nessuna parte. Voglio solo che il mio controller restituisca gli stessi oggetti che l’API generata fa, con HAL _links invece di ogni riferimento serializzato. qualche idea?

EDIT: OK, dopo aver riflettuto due volte mi sono reso conto che l’annotazione @EnableHypermediaSupport deve essere aggiunta alla configurazione , ovviamente. Questo risolve il problema dei riferimenti circolari e posso rimuovere @JsonBackReference dall’utente. Ma solo gli attributi dell’object stesso vengono serializzati, non esiste una sezione _links:

 { "amount": -4.81, "date": "2014-06-17T21:18:00.000+0000", "description": "Pizza" } 

So che potrei scrivere classi wrapper estendendo ResourceSupport per tutte le mie entity framework, ma questo sembra piuttosto inutile. Poiché spring-hateoas è in grado di generare magicamente le rappresentazioni con la sezione _link per l’interfaccia REST che viene creata automaticamente, ci dovrebbe essere un modo per restituire le stesse rappresentazioni da un controller personalizzato, giusto?

Ci sono molti aspetti qui:

  1. Dubito che la risorsa di raccolta su /transactions restituisca effettivamente una singola transazione come hai descritto. Queste rappresentazioni vengono restituite per le risorse articolo.

  2. Se TransactionRepository è già un PageableAndSortingRepository la risorsa di raccolta può essere ottimizzata espandendo il modello URI esposto nella radice dell’API per il collegamento denominato transactions . Di default questo è un parametro per page , size e sort . Ciò significa che i clienti possono richiedere ciò che si desidera esporre già.

  3. Se si desidera predefinire le opzioni di paging e di ordinamento, implementare un controller è il modo corretto. Tuttavia, per ottenere una rappresentazione come Spring Data REST espone è necessario restituire almeno istanze di ResourceSupport poiché questo è il tipo per cui è stata registrata la mapping HAL.

    Non c’è nulla di magico qui se ci pensi. Una semplice entity framework non ha link, il ResourcesSupport e tipi come Resource ti permettono di avvolgere l’entity framework e arricchirla con link come meglio credi. Spring Data REST lo fa fondamentalmente per te, usando molte delle conoscenze sul dominio e sulla struttura del repository disponibile implicitamente. Puoi riutilizzare molto come mostrato di seguito.

    Ci sono alcuni aiutanti di cui devi essere a conoscenza qui:

    • PersistentEntityResourceAssembler – che di solito viene iniettato nel metodo del controller. Rende una singola quadro in un modo REST di Spring Data, il che significa che le associazioni che puntano ai tipi gestiti saranno rese come collegamenti, ecc.
    • PagedResourcesAssembler – di solito iniettato nell’istanza del controller. Si prende cura di preparare gli articoli contenuti nella pagina, opzionalmente utilizzando un ResourceAssembler dedicato.

    Quello che Spring Data REST fondamentalmente fa per le pagine è il seguente:

     PersistentEntityResourceAssembler entityAssembler = …; Resources … = pagedResourcesAssembler.toResources(page, entityAssembler); 

    In pratica, si utilizza PagedResourcesAssembler con PersistentEntityResourceAssembler per il rendering degli elementi.

    Restituendo quell’istanza di Resources dovresti darti il ​​disegno di rappresentazione che ti aspettavi.

Non è necessario creare il proprio controller per limitare i risultati delle query o ordinare i risultati. Basta creare un metodo di query nel repository:

 public interface TransactionRepository extends MongoRepository { List findFirst10ByOrderByDateDesc(); } 

Spring Data REST lo esporterà automaticamente come risorsa del metodo in /transactions/search/findFirst10ByOrderByDateDesc .

Per utilizzare PersistentEntityResourceAssembler nel controller dovremmo contrassegnarlo come @RepositoryRestController

 @RestController @RequestMapping("/categories") @RepositoryRestController public class CategoryController implements ValidableController { // dependencies @RequestMapping(method = POST, consumes = MediaType.APPLICATION_JSON_VALUE) public ResponseEntity create(@Valid @RequestBody CategoryForm category, BindingResult validation, PersistentEntityResourceAssembler resourceAssembler) { validate(validation); Category entity = categoryConverter.convert(category); entity = categoryService.save(entity); return ResponseEntity.ok(resourceAssembler.toFullResource(entity)); } 

Costruisce una bella risposta in stile HAL

 { "createdTime": "2018-07-24T00:55:32.854", "updatedTime": "2018-07-24T00:55:32.855", "name": "cfvfcdfgdfdfdfs32", "options": [ "aaa", "bbb" ], "_links": { "self": { "href": "http://localhost:8080/shop/categories/34" }, "category": { "href": "http://localhost:8080/shop/categories/34{?projection}", "templated": true }, "products": { "href": "http://localhost:8080/shop/categories/34/products" }, "categories": { "href": "http://localhost:8080/shop/categories/34/categories{?projection}", "templated": true }, "parent": { "href": "http://localhost:8080/shop/categories/34/parent{?projection}", "templated": true } } 

}