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