28

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.

Community
  • 1
  • 1
Nicolas
  • 321
  • 2
  • 4
  • 7
  • 1
    http://docs.spring.io/spring-data/data-jpa/docs/current/reference/html/#repositories.single-repository-behaviour Does this maybe help you? – Tarmo Apr 21 '16 at 15:04
  • I realize that this question isn't a Grails question, but the concept is similar to the question/answer described here: http://stackoverflow.com/questions/19360559/adding-functionality-to-grails-restfulcontroller – rmlan Apr 21 '16 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. – Nicolas Apr 21 '16 at 15:27

4 Answers4

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
    }
}
Marc Tarin
  • 3,109
  • 17
  • 49
  • 5
    Unfortunately, that doesn't work either. If I do that, then the GET method implemented by Spring doesn't work. – Nicolas Apr 25 '16 at 16:11
  • 2
    Seems to work by me (spring-boot-starter-data-rest 1.4.1.RELEASE) Also the `@RepositoryRestController` vs `@RestController` did the trick. – Sergey Shcherbakov Dec 12 '16 at 17:12
  • 2
    Also had to add `@ResponseBody` to the overridden controller methods. – Sergey Shcherbakov Dec 12 '16 at 22:06
  • 1
    Good point @SergeyShcherbakov. I guess I copy-pasted the original method without thinking it through. Rather than using ResponseBody and other annotations (ResponseStatus and the like), my personal preference is to return a [ResponseEntity](http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/http/ResponseEntity.html), which has a few static methods for quickly building common response but also allows full control over the response headers and status. – Marc Tarin Dec 13 '16 at 09:04
  • 3
    Doing this way ou lose the HATEOAS format ... is there a option to maintain the same format? – Rafael Mar 30 '17 at 11:33
  • 3
    @Rafael: not an option. You have to use (extend) [Resource](http://docs.spring.io/spring-hateoas/docs/current/api/org/springframework/hateoas/Resource.html) and [ResourceAssemblerSupport](http://docs.spring.io/spring-hateoas/docs/current/api/org/springframework/hateoas/mvc/ResourceAssemblerSupport.html). There is information in the official [documentation](http://docs.spring.io/autorepo/docs/spring-hateoas/0.20.x/reference/html/#fundamentals.resource-assembler). You can also read [this](http://stackoverflow.com/a/29924387/5873923) and [this](http://stackoverflow.com/a/26551904/5873923) – Marc Tarin Mar 30 '17 at 13:01
11

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>
Daniel Cerecedo
  • 6,071
  • 4
  • 38
  • 51
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

azous
  • 51
  • 1
  • 2
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
    }
}
jsannn
  • 37
  • 2
  • 4
    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. – lealceldeiro Dec 01 '17 at 20:02