Spring Data REST: Override repository method on th

2019-01-14 13:10发布

问题:

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.

回答1:

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


回答2:

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:

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


回答4:

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