Spring MVC 3: retornar uma página de dados da Spring como JSON

Eu tenho uma camada de access a dados feita com Spring-Data. Agora estou criando um aplicativo da web sobre ele. Esse método de controlador único deve retornar uma página de dados do Spring formatada como JSON.

Essa Página é uma Lista com informações adicionais de Paginação, como a quantidade total de registros e assim por diante.

Isso é possível e se sim como?

E diretamente relacionado a isso posso definir o mapeamento de nomes de propriedades? Por exemplo. significando que eu precisaria definir como as propriedades da informação de paginação são nomeadas em JSON (diferentemente do que na página). Isso é possível e como?

Há suporte para um cenário como este no Spring HATEOAS e Spring Data Commons. O Spring HATEOAS vem com um object PageMetadata que essencialmente contém os mesmos dados de uma Page mas de uma maneira menos exigente, para que possa ser mais facilmente empacotado e desempacotado.

Outro aspecto da razão pela qual implementamos isso em combinação com o Spring HATEOAS e o Spring Data commons é que há pouco valor em simplesmente empacotar a página, seu conteúdo e os metadados, mas também deseja gerar os links para as próximas páginas existentes ou anteriores, para que o cliente não precisa construir URIs para percorrer essas páginas.

Um exemplo

Assuma uma class de domínio Person :

 class Person { Long id; String firstname, lastname; } 

assim como seu repository correspondente:

 interface PersonRepository extends PagingAndSortingRepository { } 

Agora você pode expor um controlador Spring MVC da seguinte maneira:

 @Controller class PersonController { @Autowired PersonRepository repository; @RequestMapping(value = "/persons", method = RequestMethod.GET) HttpEntity> persons(Pageable pageable, PagedResourcesAssembler assembler) { Page persons = repository.findAll(pageable); return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK); } } 

Há provavelmente um pouco para explicar aqui. Vamos dar um passo a passo:

  1. Nós temos um controlador Spring MVC recebendo o repository conectado a ele. Isso requer que o Spring Data esteja configurado (através dos @Enable(Jpa|Mongo|Neo4j|Gemfire)Repositories ou dos equivalentes XML). O método do controlador é mapeado para /persons , o que significa que ele aceita todas as solicitações GET para esse método.
  2. O tipo principal retornado do método é um PagedResources – um tipo do Spring HATEOAS que representa algum conteúdo enriquecido com Links mais um PageMetadata .
  3. Quando o método é invocado, o Spring MVC terá que criar instâncias para Pageable e PagedResourcesAssembler . Para que isso funcione, é necessário ativar o suporte da Web Spring Data através da anotação @EnableSpringDataWebSupport que será lançada no próximo marco do Spring Data Commons ou por meio de definições de bean independentes (documentadas aqui ).

    O Pageable será preenchido com informações da solicitação. A configuração padrão irá transformar ?page=0&size=10 em um Pageable solicitando a primeira página por um tamanho de página de 10.

    O PageableResourcesAssembler permite transformar facilmente uma Page em instâncias de PagedResources . Ele não apenas adicionará os metadados da página à resposta, mas também adicionará os links apropriados à representação com base na página acessada e na configuração da sua resolução Pageable .

Uma configuração JavaConfig de amostra para ativar isso para JPA seria assim:

 @Configuration @EnableWebMvc @EnableSpringDataWebSupport @EnableJpaRepositories class ApplicationConfig { // declare infrastructure components like EntityManagerFactory etc. here } 

Um pedido de amostra e resposta

Suponha que temos 30 Persons no database. Agora você pode acionar um pedido GET http://localhost:8080/persons e você verá algo semelhante a isto:

 { "links" : [ { "rel" : "next", "href" : "http://localhost:8080/persons?page=1&size=20 } ], "content" : [ … // 20 Person instances rendered here ], "pageMetadata" : { "size" : 20, "totalElements" : 30, "totalPages" : 2, "number" : 0 } } 

Observe que o montador produziu o URI correto e também selecionou a configuração padrão presente para resolver os parâmetros em um Pageable para uma solicitação futura. Isso significa que, se você alterar essa configuração, os links aderirão automaticamente à alteração. Por padrão, o assembler aponta para o método do controlador no qual ele foi chamado, mas que pode ser customizado, entregando um Link customizado a ser usado como base para construir os links de paginação para sobrecargas do método PagedResourcesAssembler.toResource(…) .

Outlook

Os bits do PagedResourcesAssembler estarão disponíveis na próxima versão de marco do trem de liberação do Spring Data Babbage. Já está disponível nos instantâneos atuais. Você pode ver um exemplo funcional disso em meu aplicativo de amostra Spring RESTBucks. Simplesmente clone-o, execute mvn jetty:run e curve http://localhost:8080/pages .

Oliver, sua resposta é ótima e eu marquei como resposta. Aqui apenas por completude o que eu inventei para o tempo médio que pode ser útil para outra pessoa.

Eu uso o JQuery Datatables como meu widget de grade / tabela. Ele envia um parâmetro muito específico para o servidor e excetua uma resposta muito específica: consulte http://datatables.net/usage/server-side .

Para conseguir isso, é criado um object auxiliar personalizado, refletindo quais datatables esperam. Note que getter e setter devem ser nomeados como se eles fossem outros, o json produzido está errado (nomes de propriedades com distinção entre maiúsculas e minúsculas e tabelas de dados usam essa “notação pseudo húngara” …).

 public class JQueryDatatablesPage implements java.io.Serializable { private final int iTotalRecords; private final int iTotalDisplayRecords; private final String sEcho; private final List aaData; public JQueryDatatablesPage(final List pageContent, final int iTotalRecords, final int iTotalDisplayRecords, final String sEcho){ this.aaData = pageContent; this.iTotalRecords = iTotalRecords; this.iTotalDisplayRecords = iTotalDisplayRecords; this.sEcho = sEcho; } public int getiTotalRecords(){ return this.iTotalRecords; } public int getiTotalDisplayRecords(){ return this.iTotalDisplayRecords; } public String getsEcho(){ return this.sEcho; } public List getaaData(){ return this.aaData; } } 

A segunda parte é um método no controlador de acordo:

 @RequestMapping(value = "/search", method = RequestMethod.GET, produces = "application/json") public @ResponseBody String search ( @RequestParam int iDisplayStart, @RequestParam int iDisplayLength, @RequestParam int sEcho, // for datatables draw count @RequestParam String search) throws IOException { int pageNumber = (iDisplayStart + 1) / iDisplayLength; PageRequest pageable = new PageRequest(pageNumber, iDisplayLength); Page page = compoundService.myCustomSearchMethod(search, pageable); int iTotalRecords = (int) (int) page.getTotalElements(); int iTotalDisplayRecords = page.getTotalPages() * iDisplayLength; JQueryDatatablesPage dtPage = new JQueryDatatablesPage<>( page.getContent(), iTotalRecords, iTotalDisplayRecords, Integer.toString(sEcho)); String result = toJson(dtPage); return result; } private String toJson(JQueryDatatablesPage dt) throws IOException { ObjectMapper mapper = new ObjectMapper(); mapper.registerModule(new Hibernate4Module()); return mapper.writeValueAsString(dt); } 

compoundService é apoiado por um repository Spring-Data. Ele gerencia transactions e segurança em nível de método. toJSON() método toJSON() usa o Jackson 2.0 e você precisa registrar o módulo apropriado para o mapeador, no meu caso para o hibernate 4.

Caso você tenha relacionamentos bidirecionais, é necessário anotar todas as suas classs de entidade com

 @JsonIdentityInfo(generator=ObjectIdGenerators.IntSequenceGenerator.class, property="jsonId") 

Isso permite que o Jackson 2.0 serialize dependencies circulares (não era possível na versão anterior e requer que suas entidades sejam anotadas).

Você precisará adicionar as seguintes dependencies:

  com.fasterxml.jackson.core jackson-core 2.2.1   com.fasterxml.jackson.datatype jackson-datatype-hibernate4 2.2.1   com.fasterxml.jackson.core jackson-annotations 2.2.1 jar  

Usando o Spring Boot (e para o Mongo DB), consegui fazer o seguinte com resultados bem-sucedidos:

 @RestController @RequestMapping("/product") public class ProductController { //... @RequestMapping(value = "/all", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE }) HttpEntity> get(@PageableDefault Pageable p, PagedResourcesAssembler assembler) { Page product = productRepository.findAll(p); return new ResponseEntity<>(assembler.toResource(product), HttpStatus.OK); } } 

e a class do modelo é assim:

 @Document(collection = "my_product") @Data @ToString(callSuper = true) public class Product extends BaseProduct { private String itemCode; private String brand; private String sku; }