From 5cd136dd56bcb1cd307b1ef3820aae733314ca5d Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Fri, 31 May 2013 12:03:33 +0200 Subject: [PATCH] DATACMNS-330, DATACMNS-331 - Documentation of new web support. --- src/docbkx/repositories.xml | 756 +++++++++++++++++++++++++----------- 1 file changed, 528 insertions(+), 228 deletions(-) diff --git a/src/docbkx/repositories.xml b/src/docbkx/repositories.xml index b91a467fb..7cacd1650 100644 --- a/src/docbkx/repositories.xml +++ b/src/docbkx/repositories.xml @@ -855,280 +855,303 @@ public class MyRepositoryFactoryBean<R extends JpaRepository<T, I>, T, Spring Data usage in a variety of contexts. Currently most of the integration is targeted towards Spring MVC. -
- Domain class web binding for Spring MVC +
+ Web support + + + This section contains the documentation for the Spring Data web + support as it is implemented as of Spring Data Commons in the 1.6 + range. As it the newly introduced support changes quite a lot of + things we kept the documentation of the former behavior in . + + + Spring Data modules ships with a variety of web support if the + module supports the repository programming model. The web related stuff + requires Spring MVC JARs on the classpath, some of them even provide + integration with Spring HATEOAS. + + + Spring HATEOAS - https://github.com/SpringSource/spring-hateoas + In general, the integration support is enabled by using the + @EnableSpringDataWebSupport annotation in + your JavaConfig configuration class. - Given you are developing a Spring MVC web application you - typically have to resolve domain class ids from URLs. By default your - task is to transform that request parameter or URL part into the domain - class to hand it to layers below then or execute business logic on the - entities directly. This would look something like this: - - @Controller -@RequestMapping("/users") -public class UserController { - - private final UserRepository userRepository; - - @Autowired - public UserController(UserRepository userRepository) { - Assert.notNull(repository, "Repository must not be null!"); - userRepository = userRepository; - } - - @RequestMapping("/{id}") - public String showUserForm(@PathVariable("id") Long id, Model model) { - - // Do null check for id - User user = userRepository.findOne(id); - // Do null check for user - - model.addAttribute("user", user); - return "user"; - } -} - - First you declare a repository dependency for each controller to - look up the entity managed by the controller or repository respectively. - Looking up the entity is boilerplate as well, as it's always a - findOne(…) call. Fortunately Spring provides - means to register custom components that allow conversion between a - String value to an arbitrary type. - - - PropertyEditors - - For Spring versions before 3.0 simple Java - PropertyEditors had to be used. To - integrate with that, Spring Data offers a - DomainClassPropertyEditorRegistrar, which looks - up all Spring Data repositories registered in the - ApplicationContext and registers a - custom PropertyEditor for the managed - domain class. - - <bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> - <property name="webBindingInitializer"> - <bean class="….web.bind.support.ConfigurableWebBindingInitializer"> - <property name="propertyEditorRegistrars"> - <bean class="org.springframework.data.repository.support.DomainClassPropertyEditorRegistrar" /> - </property> - </bean> - </property> -</bean> + + Enabling Spring Data web support - If you have configured Spring MVC as in the preceding example, - you can configure your controller as follows, which reduces a lot of - the clutter and boilerplate. + @Configuration +@EnableWebMvc +@EnableSpringDataWebSupport +class WebConfiguration { } + - @Controller -@RequestMapping("/users") -public class UserController { + The @EnableSpringDataWebSupport annotation registers a few + components we will discuss in a bit. It will also detect Spring HATEOAS + on the classpath and register integration components for it as well if + present. - @RequestMapping("/{id}") - public String showUserForm(@PathVariable("id") User user, Model model) { + Alternatively, if you are using XML configuration, register either + SpringDataWebSupport or HateoasAwareSpringDataWebSupport as Spring + beans: - model.addAttribute("user", user); - return "userForm"; - } -} - + + Enabling Spring Data web support in XML - - ConversionService + <bean class="org.springframework.data.web.config.SpringDataWebConfiguration" /> - In Spring 3.0 and later the - PropertyEditor support is superseded by - a new conversion infrastructure that eliminates the drawbacks of - PropertyEditors and uses a stateless X - to Y conversion approach. Spring Data now ships with a - DomainClassConverter that mimics the behavior - of DomainClassPropertyEditorRegistrar. To - configure, simply declare a bean instance and pipe the - ConversionService being used into its - constructor: +<!-- If you're using Spring HATEOAS as well register this one *instead* of the former --> +<bean class="org.springframework.data.web.config.HateoasAwareSpringDataWebConfiguration" /> + - <mvc:annotation-driven conversion-service="conversionService" /> +
+ Basic web support -<bean class="org.springframework.data.repository.support.DomainClassConverter"> - <constructor-arg ref="conversionService" /> -</bean> - - If you are using JavaConfig, you can simply extend Spring MVC's - WebMvcConfigurationSupport and hand the - FormatingConversionService that the - configuration superclass provides into the - DomainClassConverter instance you - create. + The configuration setup shown above will register a few basic + components: - class WebConfiguration extends WebMvcConfigurationSupport { + + + A DomainClassConverter to enable + Spring MVC to resolve instances of repository managed domain + classes from request parameters or path variables. + - // Other configuration omitted + + HandlerMethodArgumentResolver + implementations to let Spring MVC resolve + Pageable and + Sort instances from request + parameters. + + - @Bean - public DomainClassConverter<?> domainClassConverter() { - return new DomainClassConverter<FormattingConversionService>(mvcConversionService()); - } -} - -
+ + DomainClassConverter -
- Web pagination + The DomainClassConverter allows you to + use domain types in your Spring MVC controller method signatures + directly, so that you don't have to manually lookup the instances + via the repository: - When working with pagination in the web layer you usually have to - write a lot of boilerplate code yourself to extract the necessary - metadata from the request. The less desirable approach shown in the - example below requires the method to contain an - HttpServletRequest parameter that has to - be parsed manually. This example also omits appropriate failure - handling, which would make the code even more verbose. + + A Spring MVC controller using domain types in method + signatures - @Controller + @Controller @RequestMapping("/users") public class UserController { - // DI code omitted - - @RequestMapping - public String showUsers(Model model, HttpServletRequest request) { - - int page = Integer.parseInt(request.getParameter("page")); - int pageSize = Integer.parseInt(request.getParameter("pageSize")); - - Pageable pageable = new PageRequest(page, pageSize); + @RequestMapping("/{id}") + public String showUserForm(@PathVariable("id") User user, Model model) { - model.addAttribute("users", userService.getUsers(pageable)); - return "users"; + model.addAttribute("user", user); + return "userForm"; } } + - The bottom line is that the controller should not have to handle - the functionality of extracting pagination information from the request. - So Spring Data ships with a - PageableHandlerArgumentResolver that will do the - work for you. The Spring MVC JavaConfig support exposes a - WebMvcConfigurationSupport helper class to - customize the configuration as follows: - - @Configuration -public class WebConfig extends WebMvcConfigurationSupport { - - @Override - public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { - converters.add(new PageableHandlerArgumentResolver()); - } -} + As you can see the method receives a User instance directly + and no further lookup is necessary. The instance can be resolved by + letting Spring MVC convert the path variable into the id type of the + domain class first and eventually access the instance through + calling findOne(…) on the repository + instance registered for the domain type. + + + Currently the repository has to implement + CrudRepository to be eligible to be + discovered for conversion. + + - If you're stuck with XML configuration you can register the - resolver as follows: + + HandlerMethodArgumentResolvers for Pageable and Sort - <bean class="….web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> - <property name="customArgumentResolvers"> - <list> - <bean class="org.springframework.data.web.PageableHandlerArgumentResolver" /> - </list> - </property> -</bean> + The configuration snippet above also registers a + PageableHandlerMethodArgumentResolver as well + as an instance of + SortHandlerMethodArgumentResolver. The + registration enables Pageable and + Sort being valid controller method + arguments - When using Spring 3.0.x versions use the - PageableArgumentResolver instead. Once you've - configured the resolver with Spring MVC it allows you to simplify - controllers down to something like this: + + Using Pageable as controller method argument - @Controller + @Controller @RequestMapping("/users") public class UserController { + @Autowired UserRepository repository; + @RequestMapping public String showUsers(Model model, Pageable pageable) { - model.addAttribute("users", userRepository.findAll(pageable)); + model.addAttribute("users", repository.findAll(pageable)); return "users"; } } + - The PageableArgumentResolver automatically - resolves request parameters to build a - PageRequest instance. By default it expects the - following structure for the request parameters. - - - Request parameters evaluated by - <classname>PageableArgumentResolver</classname> - - - - - - - - - page - - Page you want to retrieve. - - - - page.size + This method signature will cause Spring MVC try to derive a + Pageable instance from the request + parameters using the following default configuration: + +
+ Request parameters evaluated for Pageable instances + + + + + + + + + page + + Page you want to retrieve. + + + + size + + Size of the page you want to retrieve. + + + + sort + + Properties that should be sorted by in the format + property,property(,ASC|DESC). Default sort + direction is ascending. Use multiple sort + parameters if you want to switch directions, e.g. + ?sort=firstname&sort=lastname,asc. + + + +
+ + To customize this behavior extend either + SpringDataWebConfiguration or the + HATEOAS-enabled equivalent and override the + pageableResolver() or + sortResolver() methods and import your + customized configuration file instead of using the + @Enable-annotation. + + In case you need multiple + Pageables or + Sorts to be resolved from the request (for + multiple tables, for example) you can use Spring's + @Qualifier annotation to distinguish + one from another. The request parameters then have to be prefixed + with ${qualifier}_. So for a method signature like + this: + + public String showUsers(Model model, + @Qualifier("foo") Pageable first, + @Qualifier("bar") Pageable second) { … } - Size of the page you want to retrieve. - + you have to populate foo_page and + bar_page etc. - - page.sort + The default Pageable handed + into the method is equivalent to a new PageRequest(0, + 20) but can be customized using the + @PageableDefaults annotation on the + Pageable parameter. +
+
- Property that should be sorted by. - +
+ Hypermedia support for Pageables - - page.sort.dir + Spring HATEOAS ships with a representation model class + PagedResources that allows enrichting the content of a Page instance + with the necessary Page metadata as well as links to let the clients + easily navigate the pages. The conversion of a Page to a + PagedResources is done by an implementation of the Spring HATEOAS + ResourceAssembler interface, the PagedResourcesAssembler. - Direction that should be used for sorting. - - - - + + Using a PagedResourcesAssembler as controller method + argument - In case you need multiple Pageables - to be resolved from the request (for multiple tables, for example) you - can use Spring's @Qualifier annotation to - distinguish one from another. The request parameters then have to be - prefixed with ${qualifier}_. So for a method signature like - this: + @Controller +class PersonController { - public String showUsers(Model model, - @Qualifier("foo") Pageable first, - @Qualifier("bar") Pageable second) { … } + @Autowired PersonRepository repository; - you have to populate foo_page and - bar_page and the related subproperties. + @RequestMapping(value = "/persons", method = RequestMethod.GET) + HttpEntity<PagedResources<Person>> persons(Pageable pageable, + PagedResourcesAssembler assembler) { - - Configuring a global default on bean declaration - - The PageableArgumentResolver will use a - PageRequest with the first page and a page size - of 10 by default. It will use that value if it cannot resolve a - PageRequest from the request (because of - missing parameters, for example). You can configure a global default - on the bean declaration directly. If you might need controller method - specific defaults for the Pageable, - annotate the method parameter with - @PageableDefaults and specify page - (through pageNumber), page size (through - value), sort (list of properties to sort - by), and sortDir (the direction to sort by) as annotation - attributes: + Page<Person> persons = repository.findAll(pageable); + return new ResponseEntity<>(assembler.toResources(persons), HttpStatus.OK); + } +} + - public String showUsers(Model model, - @PageableDefaults(pageNumber = 0, value = 30) Pageable pageable) { … } - + Enabling the configuration as shown above allows the + PagedResourcesAssembler to be used as + controller method argument. Calling + toResources(…) on it will cause the + following: + + + + The content of the Page will + become the content of the PagedResources + instance. + + + + The PagedResources will get a + PageMetadata instance attached populated + with information form the Page and + the underlying PageRequest. + + + + The PagedResources gets + prev and next links attached depending + on the page's state. The links will point to the URI the method + invoked is mapped to. The pagination parameters added to the + method will match the setup of the + PageableHandlerMethodArgumentResolver to + make sure the links can be resolved later on. + + + + Assume we have 30 Person instances 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 + } +}You see 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. +
@@ -1218,5 +1241,282 @@ processing.-->The type to which the JSON object will be unmarshalled to will </beans>
+ +
+ Legacy web support + +
+ Domain class web binding for Spring MVC + + Given you are developing a Spring MVC web application you + typically have to resolve domain class ids from URLs. By default your + task is to transform that request parameter or URL part into the + domain class to hand it to layers below then or execute business logic + on the entities directly. This would look something like this: + + @Controller +@RequestMapping("/users") +public class UserController { + + private final UserRepository userRepository; + + @Autowired + public UserController(UserRepository userRepository) { + Assert.notNull(repository, "Repository must not be null!"); + userRepository = userRepository; + } + + @RequestMapping("/{id}") + public String showUserForm(@PathVariable("id") Long id, Model model) { + + // Do null check for id + User user = userRepository.findOne(id); + // Do null check for user + + model.addAttribute("user", user); + return "user"; + } +} + + First you declare a repository dependency for each controller to + look up the entity managed by the controller or repository + respectively. Looking up the entity is boilerplate as well, as it's + always a findOne(…) call. Fortunately Spring + provides means to register custom components that allow conversion + between a String value to an arbitrary + type. + + + PropertyEditors + + For Spring versions before 3.0 simple Java + PropertyEditors had to be used. To + integrate with that, Spring Data offers a + DomainClassPropertyEditorRegistrar, which + looks up all Spring Data repositories registered in the + ApplicationContext and registers a + custom PropertyEditor for the managed + domain class. + + <bean class="….web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter"> + <property name="webBindingInitializer"> + <bean class="….web.bind.support.ConfigurableWebBindingInitializer"> + <property name="propertyEditorRegistrars"> + <bean class="org.springframework.data.repository.support.DomainClassPropertyEditorRegistrar" /> + </property> + </bean> + </property> +</bean> + + If you have configured Spring MVC as in the preceding example, + you can configure your controller as follows, which reduces a lot of + the clutter and boilerplate. + + @Controller +@RequestMapping("/users") +public class UserController { + + @RequestMapping("/{id}") + public String showUserForm(@PathVariable("id") User user, Model model) { + + model.addAttribute("user", user); + return "userForm"; + } +} + + + + ConversionService + + In Spring 3.0 and later the + PropertyEditor support is superseded + by a new conversion infrastructure that eliminates the drawbacks of + PropertyEditors and uses a stateless + X to Y conversion approach. Spring Data now ships with a + DomainClassConverter that mimics the behavior + of DomainClassPropertyEditorRegistrar. To + configure, simply declare a bean instance and pipe the + ConversionService being used into its + constructor: + + <mvc:annotation-driven conversion-service="conversionService" /> + +<bean class="org.springframework.data.repository.support.DomainClassConverter"> + <constructor-arg ref="conversionService" /> +</bean> + + If you are using JavaConfig, you can simply extend Spring + MVC's WebMvcConfigurationSupport and hand the + FormatingConversionService that the + configuration superclass provides into the + DomainClassConverter instance you + create. + + class WebConfiguration extends WebMvcConfigurationSupport { + + // Other configuration omitted + + @Bean + public DomainClassConverter<?> domainClassConverter() { + return new DomainClassConverter<FormattingConversionService>(mvcConversionService()); + } +} + +
+ +
+ Web pagination + + When working with pagination in the web layer you usually have + to write a lot of boilerplate code yourself to extract the necessary + metadata from the request. The less desirable approach shown in the + example below requires the method to contain an + HttpServletRequest parameter that has + to be parsed manually. This example also omits appropriate failure + handling, which would make the code even more verbose. + + @Controller +@RequestMapping("/users") +public class UserController { + + // DI code omitted + + @RequestMapping + public String showUsers(Model model, HttpServletRequest request) { + + int page = Integer.parseInt(request.getParameter("page")); + int pageSize = Integer.parseInt(request.getParameter("pageSize")); + + Pageable pageable = new PageRequest(page, pageSize); + + model.addAttribute("users", userService.getUsers(pageable)); + return "users"; + } +} + + The bottom line is that the controller should not have to handle + the functionality of extracting pagination information from the + request. So Spring Data ships with a + PageableHandlerArgumentResolver that will do + the work for you. The Spring MVC JavaConfig support exposes a + WebMvcConfigurationSupport helper class to + customize the configuration as follows: + + @Configuration +public class WebConfig extends WebMvcConfigurationSupport { + + @Override + public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { + converters.add(new PageableHandlerArgumentResolver()); + } +} + + If you're stuck with XML configuration you can register the + resolver as follows: + + <bean class="….web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter"> + <property name="customArgumentResolvers"> + <list> + <bean class="org.springframework.data.web.PageableHandlerArgumentResolver" /> + </list> + </property> +</bean> + + When using Spring 3.0.x versions use the + PageableArgumentResolver instead. Once you've + configured the resolver with Spring MVC it allows you to simplify + controllers down to something like this: + + @Controller +@RequestMapping("/users") +public class UserController { + + @RequestMapping + public String showUsers(Model model, Pageable pageable) { + + model.addAttribute("users", userRepository.findAll(pageable)); + return "users"; + } +} + + The PageableArgumentResolver + automatically resolves request parameters to build a + PageRequest instance. By default it expects the + following structure for the request parameters. + + + Request parameters evaluated by + <classname>PageableArgumentResolver</classname> + + + + + + + + + page + + Page you want to retrieve. + + + + page.size + + Size of the page you want to retrieve. + + + + page.sort + + Property that should be sorted by. + + + + page.sort.dir + + Direction that should be used for sorting. + + + +
+ + In case you need multiple + Pageables to be resolved from the + request (for multiple tables, for example) you can use Spring's + @Qualifier annotation to distinguish + one from another. The request parameters then have to be prefixed with + ${qualifier}_. So for a method signature like + this: + + public String showUsers(Model model, + @Qualifier("foo") Pageable first, + @Qualifier("bar") Pageable second) { … } + + you have to populate foo_page and + bar_page and the related subproperties. + + + Configuring a global default on bean declaration + + The PageableArgumentResolver will use a + PageRequest with the first page and a page + size of 10 by default. It will use that value if it cannot resolve a + PageRequest from the request (because of + missing parameters, for example). You can configure a global default + on the bean declaration directly. If you might need controller + method specific defaults for the + Pageable, annotate the method + parameter with @PageableDefaults and + specify page (through pageNumber), page size (through + value), sort (list of properties to sort + by), and sortDir (the direction to sort by) as + annotation attributes: + + public String showUsers(Model model, + @PageableDefaults(pageNumber = 0, value = 30) Pageable pageable) { … } + +
+