Spring Data REST:在控制器上覆盖存储库方法

28

我有一个REST存储库,其实现是由Spring在运行时生成的。

@RepositoryRestResource
public interface FooRepository extends CrudRepository<Foo, Long> {

}

这意味着我可以通过REST使用 save()、find()、exists() 和其他方法。

现在,我想重写其中一个方法,例如save()。为此,我将创建一个公开该方法的控制器,如下所示:

@RepositoryRestController
@RequestMapping("/foo")
public class FooController {

    @Autowired
    FooService fooService;


    @RequestMapping(value = "/{fooId}", method = RequestMethod.PUT)
    public void updateFoo(@PathVariable Long fooId) {
        fooService.updateProperly(fooId);
    }

}

问题:

如果我启用这个控制器,那么Spring实现的所有其他方法都将不再暴露。例如,我不能再对/foo/1进行GET请求了。

问题:

是否有一种方法可以覆盖REST方法,同时仍保留其他自动生成的Spring方法?

额外信息:

  1. 这个问题似乎非常相似: Spring Data Rest: Override Method in RestController with same request-mapping-path ... 但是我不想把路径改为像/foo/1/save这样的东西

  2. 我想使用@RepositoryEventHandler,但我不太喜欢这个想法,因为我想将其封装在一个服务下。此外,您似乎失去了事务上下文的控制。

  3. Spring Data文档的这部分内容说:

    有时你可能想为特定资源编写自定义处理程序。要利用Spring Data REST的设置、消息转换器、异常处理等, 请使用@RepositoryRestController注释,而不是标准的Spring MVC@Controller或@RestController。

所以看起来应该可以直接工作,但不幸的是并不是这样。


1
这个可能会对你有所帮助吗?http://docs.spring.io/spring-data/data-jpa/docs/current/reference/html/#repositories.single-repository-behaviour - Tarmo
我意识到这个问题并不是一个Grails问题,但是概念与这里描述的问题/答案相似:http://stackoverflow.com/questions/19360559/adding-functionality-to-grails-restfulcontroller - rmlan
@Tarmo:虽然我认为这可能有效,但它会强制我不断将逻辑添加到存储库中,而我更喜欢将其保留在服务中。 - Nicolas
4个回答

15

是否有一种方法可以在保留其他自动生成的Spring方法的同时覆盖REST方法?

仔细查看文档中的示例:虽然没有明确禁止类级别的请求映射,但它使用方法级别的请求映射。 我不确定这是否是期望的行为还是错误,但据我所知,这是使其工作的唯一方法,如此处所述。

只需将您的控制器更改为:

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

5
很遗憾,这也行不通。如果我这样做,Spring 实现的 GET 方法也会失效。 - Nicolas
2
似乎对我有效(spring-boot-starter-data-rest 1.4.1.RELEASE)。 此外,@RepositoryRestController@RestController的区别解决了问题。 - Sergey Shcherbakov
2
还必须在重写的控制器方法中添加@ResponseBody - Sergey Shcherbakov
1
很好的观点@SergeyShcherbakov。我想我在没有深思熟虑的情况下复制粘贴了原始方法。与使用ResponseBody和其他注释(如ResponseStatus等)不同,我个人更喜欢返回ResponseEntity,它有一些静态方法可以快速构建常见响应,但也允许完全控制响应头和状态。 - Marc Tarin
3
这样做会失去HATEOAS格式... 是否有保持相同格式的选项? - Rafael
3
@Rafael:这不是一个选项。你必须使用(扩展)Resource和ResourceAssemblerSupport。官方文档中有相关信息。你也可以阅读这个和这个Stack Overflow的帖子。 - Marc Tarin

11

让我们设想我们有一个账户(Account)实体:

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

有一个AccountRepository将其CRUD端点公开在/accounts上:

@RepositoryRestResource(collectionResourceRel = "accounts", path = "accounts")
public interface AccountRepository extends CrudRepository<Account, Integer> {
} 

还需要一个AccountController,它覆盖了AccountRepository中默认的GET端点表单。

@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);
    }

如果您调用 GET /accounts?size=5&page=0,您将得到以下使用模拟实现的输出:

{
  "_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
  }
}

为了完整起见,POM可以配置以下父级和依赖项:

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

这就是答案! - SingleShot
ResourceAssembler是来自HATEOAS 1.0的RepresentationModelAssembler。请参见https://github.com/spring-projects/spring-hateoas/blob/master/etc/migrate-to-1.0.sh。 - Lubo
但是这个能和请求参数一起使用吗? - 9900kf

5

我有一份更新,它救了我的命。正如@mathias-dpunkt在这个答案中所说的那样:

https://dev59.com/jVsW5IYBdhLWcg3w2qRM#34518166

最重要的是RepositoryRestController知道spring data rest基本路径,并将在此基本路径下提供服务。

因此,如果你的基本路径是“/api”并且你使用@RepositoryRestController,则必须从@RequestMapping中省略“/api”。


2

如果你使用Java 8,我发现了一个很棒的解决方案 - 只需在接口中使用默认方法即可。

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

4
这将覆盖整个应用程序对于此代码库的“save”方法。如果这不是期望的行为,则不应使用它,否则它是一个有效的选项。 - lealceldeiro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接