Skip to content

Dr. Will Faithfull

Overriding Spring Data REST repositories

java, spring, spring-data-rest3 min read

We often use spring-data-rest to abstract away simple RESTful CRUD boilerplate, and it is very good at it. The problem comes in more complicated situations. Sometimes you need to step in and override one of the request handlers to do something slightly differently, or trigger an event. So, how do you reliably override request handlers for repositories which are exported with @RepositoryRestResource?

The way to do so is poorly documented, and contains several caveats, which I will share from my experience. All we get in the documentation is this little snippet in section 16.4.

1@RepositoryRestController
2public class ScannerController {
3
4 private final ScannerRepository repository;
5
6 @Autowired
7 public ScannerController(ScannerRepository repo) {
8 repository = repo;
9 }
10
11 @RequestMapping(method = GET, value = "/scanners/search/listProducers")
12 public @ResponseBody ResponseEntity<?> getProducers() {
13 List<String> producers = repository.listProducers();
14
15 //
16 // do some intermediate processing, logging, etc. with the producers
17 //
18
19 Resources<String> resources = new Resources<String>(producers);
20
21 resources.add(linkTo(methodOn(ScannerController.class).getProducers()).withSelfRel());
22
23 // add other links as needed
24
25 return ResponseEntity.ok(resources);
26 }
27
28}

What is wrong with this? Just one thing really. It adds a new method to the repository route, rather than overriding an existing one. Importantly, it doesn't mention that this approach will expressly not allow you to override existing handlers. So, how do we define a controller which takes precedence over a repository in the event that both sit on the same path?

From the top..

Let's say we have an exported FooRepository.

1@RepositoryRestResource(exported=true, path="foos")
2public interface FooRepository extends PagingAndSortingRepository<Foo,Long> {
3
4}

This exposes the basic CRUD:

  • GET /foos
  • POST /foos
  • GET /foos/{id}
  • PUT /foos/{id}
  • POST /foos/{id}
  • PATCH /foos/{id}
  • DELETE /foos/{id}
  • HEAD /foos/{id}

Along with any association links rendered.

Let's say we don't want to return ALL Foos on GET /foos, but just the ones associated with our member.

How to do it

We make an overriding controller.

1@BasePathAwareController
2public class FooController {
3
4 private final FooRepository fooRepository;
5
6 public FooController(final FooRepository fooRepository) {
7 this.fooRepository = fooRepository;
8 }
9
10 @RequestMapping(path="foos", method=RequestMethod.GET, produces="application/hal+json")
11 public Resources getAllFooFiltered() {
12 // Do your filtering and end up with a HATEOAS resources to return
13 }
14
15}

That's it - a request to /foos will hit your controller now. Any other requests in the table above will still go to the exported repository handlers.

Gotchas / FAQ

  1. It must be @BasePathAwareController. @RepositoryRestController will not override exported handlers, but you can use it to add handlers that aren't exported on the repository.
  • You must NOT have a @RequestMapping at the type level - it has to be at the method level as above.
  • The path in your @RequestMapping(path="...") must NOT start with a /
    • OK: @RequestMapping(path="foos")
    • NOT OK: @RequestMapping(path="/foos")
  • You cannot define a standalone mapping like foos/count, because it clashes with foos/{id}. If you need to do this, you must override foos/{id} and decide what do do based on the {id} path variable content. (e.g. if "count".equals(id) { ... }). This is sort of an antipattern anyway.
  • You must NOT have any @PreAuthorize annotations on the type or methods - this causes the class to be proxied and prevents the desired behaviour. If you need security rules, implement them in a service layer below the controller, or wait for Spring to fix. DATAREST-535

Appendix - @PreAuthorize and controllers

Attempting to apply @PreAuthorize annotations on controller methods has been the cause of a lot of mysterious behaviour. A primary issue is the difference between CGLIB and JDK Dynamic proxies. JDK dynamic proxies are what spring uses by default if you have @EnableAspectJAutoProxy - but they only work for classes that are interfaced. CGLIB proxies are more clever. You enable them with @EnableAspectJAutoProxy(proxyTargetClass=true), and they will actually create a dynamic subclass of the targeted type at runtime, thus allowing proxies of interface-less classes. However, this doesn't play nice with Spring Data REST, for reasons which I have not yet had time to investigate.

If we apply some security annotations to the above overriding controller, and then try to hit one of the repository methods, what happens?

1{
2 "timestamp": "2017-05-04T08:23:46.090+0000",
3 "status": 405,
4 "error": "Method Not Allowed",
5 "exception": "org.springframework.web.HttpRequestMethodNotSupportedException",
6 "message": "Request method 'PATCH' not supported",
7 "path": "/foos/1"
8}

So, that's now no good. We can offer our own routes, but can't hit the existing repository routes.

Part of the reason this is particularly prevalent in controller classes is that they almost never implement interfaces, so their dynamic proxies (for @PreAuthorize or @Secured) will be CGLIB. The bulletproof solution at the moment is to do your fancy authorization annotations at the service layer. In fact, this is what is advocated in the spring security documentation itself (important part is highlighted at the bottom).

44.2.16 I have added Spring Security’s <global-method-security> element to my application context but if I add security annotations to my Spring MVC controller beans (Struts actions etc.) then they don’t seem to have an effect.

In a Spring web application, the application context which holds the Spring MVC beans for the dispatcher servlet is often separate from the main application context. It is often defined in a file called myapp-servlet.xml, where "myapp" is the name assigned to the Spring DispatcherServlet in web.xml. An application can have multiple DispatcherServlets, each with its own isolated application context. The beans in these "child" contexts are not visible to the rest of the application. The"parent" application context is loaded by the ContextLoaderListener you define in your web.xml and is visible to all the child contexts. This parent context is usually where you define your security configuration, including the <global-method-security> element). As a result any security constraints applied to methods in these web beans will not be enforced, since the beans cannot be seen from the DispatcherServlet context. You need to either move the <global-method-security> declaration to the web context or moved the beans you want secured into the main application context. Generally we would recommend applying method security at the service layer rather than on individual web controllers.

© 2020 by Dr. Will Faithfull. All rights reserved.
Theme by LekoArts