I have the following REST repository, whose implementation is generated at runtime by Spring.
@RepositoryRestResource public interface FooRepository extends CrudRepository<Foo, Long> { }
This means that I will have save(), find(), exists() and other methods available and exposed via REST.
Now, I would like to override one of the methods; for example, save(). For that, I would create a controller exposing that method, like so:
@RepositoryRestController @RequestMapping("/foo") public class FooController { @Autowired FooService fooService; @RequestMapping(value = "/{fooId}", method = RequestMethod.PUT) public void updateFoo(@PathVariable Long fooId) { fooService.updateProperly(fooId); } }
The problem: If I enable this controller, then all of the other methods implemented by Spring are not exposed anymore. So, for example, I can no longer do a GET request to /foo/1
Question: Is there a way of overriding REST methods while still keeping the other auto-generated Spring methods?
Extra info:
This question seems very similar: Spring Data Rest: Override Method in RestController with same request-mapping-path ... but I don't want to change the path to something like /foo/1/save
I thought of using a @RepositoryEventHandler but I'm not very fond of that idea because I would like to encapsulate it under a service. Also, you seem to lose control of the transaction context.
This part of the Spring Data documentation says the following:
Sometimes you may want to write a custom handler for a specific resource. To take advantage of Spring Data REST’s settings, message converters, exception handling, and more, use the @RepositoryRestController annotation instead of a standard Spring MVC @Controller or @RestController
so it seems that it should work out of the box, but unfortunately not.
Spring Data REST can be used to expose HATEOAS RESTful resources around Spring Data repositories. Without writing a lot of code, we can expose RESTful API around Spring Data Repositories.
@RepositoryRestResource is used to set options on the public Repository interface - it will automatically create endpoints as appropriate based on the type of Repository that is being extended (i.e. CrudRepository/PagingAndSortingRepository/etc).
@RepositoryRestResource:This annotation gives RestController functionality to the repository. This means you can access your Repository directly.
The spring-data-rest-webmvc is the project describing the main concepts of spring-data-rest which is the one of the main spring modules. In most cases one will use spring-data-rest dependency for his/her project.
Is there a way of overriding REST methods while still keeping the other auto-generated Spring methods?
Look at the example in the documentation carefully: while not explicitly forbidding class-level requestmapping, it uses method-level requestmapping. I'm not sure if this is the wanted behavior or a bug, but as far as I know this is the only way to make it work, as stated here.
Just change your controller to:
@RepositoryRestController public class FooController { @Autowired FooService fooService; @RequestMapping(value = "/foo/{fooId}", method = RequestMethod.PUT) public void updateFoo(@PathVariable Long fooId) { fooService.updateProperly(fooId); } // edited after Sergey's comment @RequestMapping(value = "/foo/{fooId}", method = RequestMethod.PUT) public RequestEntity<Void> updateFoo(@PathVariable Long fooId) { fooService.updateProperly(fooId); return ResponseEntity.ok().build(); // simplest use of a ResponseEntity } }
Let's imagine we have an Account
entity:
@Entity public class Account implements Identifiable<Integer>, Serializable { private static final long serialVersionUID = -3187480027431265380L; @Id private Integer id; private String name; public Account(Integer id, String name) { this.id = id; this.name = name; } public void setId(Integer id) { this.id = id; } @Override public Integer getId() { return id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
With an AccountRepository
exposing its CRUD endpoints on /accounts
:
@RepositoryRestResource(collectionResourceRel = "accounts", path = "accounts") public interface AccountRepository extends CrudRepository<Account, Integer> { }
And an AccountController
that overrides the default GET
endpoint form AccountRepository
.:
@RepositoryRestController public class AccountController { private PagedResourcesAssembler<Account> pagedAssembler; @Autowired public AccountController(PagedResourcesAssembler<Account> pagedAssembler) { this.pagedAssembler = pagedAssembler; } private Page<Account> getAccounts(Pageable pageRequest){ int totalAccounts= 50; List<Account> accountList = IntStream.rangeClosed(1, totalAccounts) .boxed() .map( value -> new Account(value, value.toString())) .skip(pageRequest.getOffset()) .limit(pageRequest.getPageSize()) .collect(Collectors.toList()); return new PageImpl(accountList, pageRequest, totalAccounts); } @RequestMapping(method= RequestMethod.GET, path="/accounts", produces = "application/hal+json") public ResponseEntity<Page<Account>> getAccountsHal(Pageable pageRequest, PersistentEntityResourceAssembler assembler){ return new ResponseEntity(pagedAssembler.toResource(getAccounts(pageRequest), (ResourceAssembler) assembler), HttpStatus.OK); }
If you invoke the GET /accounts?size=5&page=0
you will get the following output which is using the mock implementation:
{ "_embedded": { "accounts": [ { "name": "1", "_links": { "self": { "href": "http://localhost:8080/accounts/1" }, "account": { "href": "http://localhost:8080/accounts/1" } } }, { "name": "2", "_links": { "self": { "href": "http://localhost:8080/accounts/2" }, "account": { "href": "http://localhost:8080/accounts/2" } } }, { "name": "3", "_links": { "self": { "href": "http://localhost:8080/accounts/3" }, "account": { "href": "http://localhost:8080/accounts/3" } } }, { "name": "4", "_links": { "self": { "href": "http://localhost:8080/accounts/4" }, "account": { "href": "http://localhost:8080/accounts/4" } } }, { "name": "5", "_links": { "self": { "href": "http://localhost:8080/accounts/5" }, "account": { "href": "http://localhost:8080/accounts/5" } } } ] }, "_links": { "first": { "href": "http://localhost:8080/accounts?page=0&size=5" }, "self": { "href": "http://localhost:8080/accounts?page=0&size=5" }, "next": { "href": "http://localhost:8080/accounts?page=1&size=5" }, "last": { "href": "http://localhost:8080/accounts?page=9&size=5" } }, "page": { "size": 5, "totalElements": 50, "totalPages": 10, "number": 0 } }
Just for the sake of completeness, the POM could be configured with the following parent and dependencies:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-rest-webmvc</artifactId> <version>2.6.1.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> </dependency> </dependencies>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With