Spring MVC 3: return a Spring-Data Page as JSON
Asked Answered
C

3

26

I have a data access layer made with Spring-Data. I'm now creating a web application on top of it. This one controller method should return a Spring-Data Page formatted as JSON.

Such a Page is a List with additional Paging info like total amount of records and so forth.

Is that possible and if yes how?

And directly related to that can I define the mapping of property names? Eg. meaning I would need define how the paging info properties are named in JSON (differently than in page). Is this possible and how?

Chelton answered 28/5, 2013 at 11:0 Comment(0)
T
60

There's support for a scenario like this upcoming in Spring HATEOAS and Spring Data Commons. Spring HATEOAS comes with a PageMetadata object that essentially contains the same data as a Page but in a less enforcing manner, so that it can be more easily marshaled and unmarshaled.

Another aspect of the reason we implement this in combination with Spring HATEOAS and Spring Data commons is that there's little value in simply marshaling the page, it's content and the metadata but also want to generate the links to maybe existing next or previous pages, so that the client doesn't have to construct URIs to traverse these pages itself.

An example

Assume a domain class Person:

class Person {

  Long id;
  String firstname, lastname;
}

as well as it's corresponding repository:

interface PersonRepository extends PagingAndSortingRepository<Person, Long> { }

You can now expose a Spring MVC controller as follows:

@Controller
class PersonController {

  @Autowired PersonRepository repository;

  @RequestMapping(value = "/persons", method = RequestMethod.GET)
  HttpEntity<PagedResources<Person>> persons(Pageable pageable, 
    PagedResourcesAssembler assembler) {

    Page<Person> persons = repository.findAll(pageable);
    return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK);
  }
}

There's probably quite a bit to explain here. Let's take it step by step:

  1. We have a Spring MVC controller getting the repository wired into it. This requires Spring Data being set up (either through @Enable(Jpa|Mongo|Neo4j|Gemfire)Repositories or the XML equivalents). The controller method is mapped to /persons, which means it will accept all GET requests to that method.
  2. The core type returned from the method is a PagedResources - a type from Spring HATEOAS that represents some content enriched with Links plus a PageMetadata.
  3. When the method is invoked, Spring MVC will have to create instances for Pageable and PagedResourcesAssembler. To get this working you need to enable the Spring Data web support either through the @EnableSpringDataWebSupport annotation about to be introduced in the upcoming milestone of Spring Data Commons or via standalone bean definitions (documented here).

    The Pageable will be populated with information from the request. The default configuration will turn ?page=0&size=10 into a Pageable requesting the first page by a page size of 10.

    The PageableResourcesAssembler allows you to easily turn a Page into a PagedResources instances. It will not only add the page metadata to the response but also add the appropriate links to the representation based on what page you access and how your Pageable resolution is configured.

A sample JavaConfig configuration to enable this for JPA would look like this:

@Configuration
@EnableWebMvc
@EnableSpringDataWebSupport
@EnableJpaRepositories
class ApplicationConfig {

  // declare infrastructure components like EntityManagerFactory etc. here
}

A sample request and response

Assume we have 30 Persons in the database. You can now trigger a request GET http://localhost:8080/persons and you'll see something similar to this:

{ "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
  }
}

Note that the assembler produced the correct URI and also picks up the default configuration present to resolve the parameters into a Pageable for an upcoming request. This means, if you change that configuration, the links will automatically adhere to the change. By default the assembler points to the controller method it was invoked in but that can be customized by handing in a custom Link to be used as base to build the pagination links to overloads of the PagedResourcesAssembler.toResource(…) method.

Outlook

The PagedResourcesAssembler bits will be available in the upcoming milestone release of the Spring Data Babbage release train. It's already available in the current snapshots. You can see a working example of this in my Spring RESTBucks sample application. Simply clone it, run mvn jetty:run and curl http://localhost:8080/pages.

Tightrope answered 28/5, 2013 at 14:33 Comment(7)
wow. sounds great. One question: Is it possible to customize how that works? Different "JavaScript Grids" sent different parameters for paging to server and require certain properties to be set in the response. In m case I mostly use datatables.net.Chelton
Yes, just customize the PageableHandlerMethodArgumentResolver Spring bean (Javadoc here). This will cause both the Pageable resolved as configured as well as the pagination links rendered accordingly.Tightrope
@OliverGierke with configuration defined at XML I get the following error: Cannot convert value of type [org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration] to required type [org.springframework.web.method.support.HandlerMethodArgumentResolver] for property 'customArgumentResolvers[0]': no matching editors or conversion strategy found. With Java cofiguration everythink works fine.Agio
@OliverGierke: How about if the application if RESTful? I mean we use the same controller to serve the requests like person.jsp and person.json?Marlie
The Spring repositories return properties such as "_links" and "_embedded". It would be nice if we could generate something identical so our client code will work regardless.Massicot
The current version of Spring RESTBucks doesn't have any use of PagedResourcesAssembler at all.Arguello
The Spring documentation for @EnableSpringDataWebSupport has been moved to docs.spring.io/spring-data/commons/docs/current/reference/html/…Hendry
C
5

Oliver, your answer is great and I mark it as answer. Here just for completeness what I came up with for the mean time which might be useful for someone else.

I use JQuery Datatables as my grid/table widget. It sends very specific parameter to server and excepts a very specific response: see http://datatables.net/usage/server-side.

To achieve this is created a custom helper object reflecting what datatables expects. Note that getter and setter must be named like they are else the produced json is wrong (case sensitive property names and datatables uses this "pseudo Hungarian notation"...).

public class JQueryDatatablesPage<T> implements java.io.Serializable {

    private final int iTotalRecords;
    private final int iTotalDisplayRecords;
    private final String sEcho;
    private final List<T> aaData;

    public JQueryDatatablesPage(final List<T> 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<T> getaaData(){
        return this.aaData;
    }
}

The second part is a method in the according controller:

@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<SimpleCompound> page = compoundService.myCustomSearchMethod(search, pageable);
    int iTotalRecords = (int) (int) page.getTotalElements();
    int iTotalDisplayRecords = page.getTotalPages() * iDisplayLength;
    JQueryDatatablesPage<SimpleCompound> 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 is backed by a Spring-Data repository. It manages transactions and method level security. toJSON() method uses Jackson 2.0 and you need to register the appropriate module to the mapper, in my case for hibernate 4.

In case you have bidirectional relationships, you need to annotate all your entity classes with

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

This enables Jackson 2.0 to serialize circular dependencies (was not possible in earlier version and requires that your entities are annotated).

You will need to add following dependencies:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.2.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-hibernate4</artifactId>
    <version>2.2.1</version>
</dependency>
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-annotations</artifactId>
    <version>2.2.1</version>
    <type>jar</type>
</dependency>
Chelton answered 29/5, 2013 at 5:17 Comment(0)
C
0

Using Spring Boot (and for Mongo DB) I was able to do the following with successful results:

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

and the model class is like this:

@Document(collection = "my_product")
@Data
@ToString(callSuper = true)
public class Product extends BaseProduct {
    private String itemCode;
    private String brand;
    private String sku;    
}
Consummation answered 11/9, 2018 at 14:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.