29

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:

  1. 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

  2. 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.

  3. 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.

3
  • 1
    docs.spring.io/spring-data/data-jpa/docs/current/reference/html/… Does this maybe help you? Commented Apr 21, 2016 at 15:04
  • I realize that this question isn't a Grails question, but the concept is similar to the question/answer described here: stackoverflow.com/questions/19360559/… Commented Apr 21, 2016 at 15:14
  • @Tarmo: While I think that may possibly work, it would force me to keep adding logic into a repository, and I prefer to keep that in a service. Commented Apr 21, 2016 at 15:27

4 Answers 4

15

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 } } 
Sign up to request clarification or add additional context in comments.

6 Comments

Unfortunately, that doesn't work either. If I do that, then the GET method implemented by Spring doesn't work.
Seems to work by me (spring-boot-starter-data-rest 1.4.1.RELEASE) Also the @RepositoryRestController vs @RestController did the trick.
Also had to add @ResponseBody to the overridden controller methods.
Doing this way ou lose the HATEOAS format ... is there a option to maintain the same format?
@Rafael: not an option. You have to use (extend) Resource and ResourceAssemblerSupport. There is information in the official documentation. You can also read this and this
|
12

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> 

3 Comments

This is the answer!
ResourceAssembler is from hateoas 1.0 called RepresentationModelAssembler. See github.com/spring-projects/spring-hateoas/blob/master/etc/…
but can this be used with request params?
5

Just an update that I found that saved my life. As said brilliantly by @mathias-dpunkt in this answer https://stackoverflow.com/a/34518166/2836627

Most importantly the RepositoryRestController is aware of the spring data rest base path and will be served under this base path.

So if your base path is "/api" and you are using @RepositoryRestController

you have to ommit "/api" from @RequestMapping

Comments

2

I found a neat solution if you are using Java 8 - just use default methods in interface

@RepositoryRestResource public interface FooRepository extends CrudRepository<Foo, Long> { default <S extends T> S save(S var1) { //do some work here } } 

1 Comment

This will override the save method for the entire application for this repository. If that's not the desired behavior this should not be used, otherwise it's a valid option.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.