如何简单地添加指向 Spring Data REST 实体的链接

Posted

技术标签:

【中文标题】如何简单地添加指向 Spring Data REST 实体的链接【英文标题】:How can I simply add a link to a Spring Data REST Entity 【发布时间】:2016-03-21 10:48:36 【问题描述】:

我的实体使用 Spring Data JPA,但为了生成关于它们的统计信息,我在 Spring @Repository 中使用 jOOQ。

由于我的方法返回实体的ListDouble,我如何将它们公开为链接?假设我有一个User 实体,我想获得以下 JSON:


  "_embedded" : 
    "users" : [ ]
  ,
  "_links" : 
    "self" : 
      "href" : "http://localhost:8080/api/users"
    ,
    "stats" : 
      "href" : "http://localhost:8080/api/users/stats"
    
    "profile" : 
      "href" : "http://localhost:8080/api/profile/users"
    
  ,
  "page" : 
    "size" : 20,
    "totalElements" : 0,
    "totalPages" : 0,
    "number" : 0
  
 

在http://localhost:8080/api/users/stats 中,我想获取我在 jOOQ 存储库中声明的方法的链接列表。我将如何处理这个问题?谢谢。

【问题讨论】:

【参考方案1】:

从docs看到这个

@Bean
public ResourceProcessor<Resource<Person>> personProcessor() 

   return new ResourceProcessor<Resource<Person>>() 

     @Override
     public Resource<Person> process(Resource<Person> resource) 

      resource.add(new Link("http://localhost:8080/people", "added-link"));
      return resource;
     
   ;

【讨论】:

spring-boot 2.1 中的测试。如果Resource&lt;ManagedEntity&gt;PagedResources 都出现在响应中,则会引发异常。请参阅下面的解决方案。【参考方案2】:

添加链接的最佳方式是考虑使用 Spring-HATEOAS,它可以让代码看起来更干净。

忠告:始终使用 org.springframework.http.ResponseEntity 向客户端返回响应,因为它可以轻松自定义响应。

因此,由于您的要求是在响应中发送链接,因此建议的最佳实践是使用 ResourceSupport(org.springframework.hateoas.ResourceSupport)ResourceAssemblerSupport( org.springframework.hateoas.mvc.ResourceAssemblerSupport) 创建需要发送给客户端的资源。

例如: 如果您有一个像 Account 这样的模型对象,那么必须有一些字段是您不希望客户知道或包含在响应中的,因此要从响应中排除这些属性,我们可以使用 ResourceAssemblerSupport 类'

public TResource toResource(T t);

从需要作为响应发送的模型对象生成资源的方法。

例如,我们有一个 Account 类(可以直接用于所有服务器端交互和操作)

@Document(collection = "Accounts_Details")

public class Account 

    @Id
    private String id;

    private String username;
    private String password;
    private String firstName;
    private String lastName;
    private String emailAddress;
    private String role;
    private boolean accountNonExpired;
    private boolean accountNonLocked;
    private boolean credentialsNonExpired;
    private boolean enabled;
    private long accountNonLockedCounter;
    private Date lastPasswordResetDate;
    private Address address;
    private long activationCode;

    public Account() 
    

    //getters and setters

现在从这个 POJO 我们将创建一个资源对象,该对象将被发送到带有选定属性的客户端。

为此,我们将创建一个帐户资源,其中仅包含客户可以查看的必要字段。然后我们创建另一个类。

@XmlRootElement

public class AccountResource extends ResourceSupport 

    @XmlAttribute
    private String username;
    @XmlAttribute
    private String firstName;
    @XmlAttribute
    private String lastName;
    @XmlAttribute
    private String emailAddress;
    @XmlAttribute
    private Address address;
    public String getUsername() 
        return username;
    
    public void setUsername(String username) 
        this.username = username;
    
    public String getFirstName() 
        return firstName;
    
    public void setFirstName(String firstName) 
        this.firstName = firstName;
    
    public String getLastName() 
        return lastName;
    
    public void setLastName(String lastName) 
        this.lastName = lastName;
    
    public String getEmailAddress() 
        return emailAddress;
    
    public void setEmailAddress(String emailAddress) 
        this.emailAddress = emailAddress;
    
    public Address getAddress() 
        return address;
    
    public void setAddress(Address address) 
        this.address = address;
    



所以现在这个资源是客户将看到或必须使用的。

创建 AccountResource 的蓝图后,我们需要一种方法将模型 POJO 转换为该资源,为此建议的最佳做法是创建 ResourceAssemblerSupport 类并覆盖 toResource(T t) 方法。

import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.hateoas.mvc.ResourceAssemblerSupport;
import org.springframework.stereotype.Component;

import com.brx.gld.www.api.controller.RegistrationController;
import com.brx.gld.www.api.model.Account;

@Component
public class AccountResourceAssembler extends ResourceAssemblerSupport<Account, AccountResource> 

    public AccountResourceAssembler(Class<RegistrationController> controllerClass,
            Class<AccountResource> resourceType) 
        super(controllerClass, resourceType);
    

    public AccountResourceAssembler() 
        this(RegistrationController.class, AccountResource.class);
    

    @Override
    public AccountResource toResource(Account account) 
        AccountResource accountResource =  instantiateResource(account); //or createResourceWithId(id, entity) canbe used which will automatically create a link to itself.
        accountResource.setAddress(account.getAddress());
        accountResource.setFirstName(account.getFirstName());
        accountResource.setLastName(account.getLastName());
        accountResource.setEmailAddress(account.getEmailAddress());
        accountResource.setUsername(account.getUsername());
        accountResource.removeLinks();
        accountResource.add(ControllerLinkBuilder.linkTo(RegistrationController.class).slash(account.getId()).withSelfRel());
        return accountResource;
    


在 toReource 方法中,我们必须使用 createdResourceWithId(id, entity) 而不是使用 instanriateReource(..),然后将 custum 链接添加到资源,这实际上也是一个值得考虑的最佳实践,但为了演示我使用了 instantiateResource(..)

现在在 Controller 中使用它:

@Controller
@RequestMapping("/api/public/accounts")
public class RegistrationController 

    @Autowired
    private AccountService accountService;

    @Autowired
    private AccountResourceAssembler accountResourceAssembler;

    @RequestMapping(method = RequestMethod.GET)
    public ResponseEntity<List<AccountResource>> getAllRegisteredUsers() 
        List<AccountResource> accountResList = new ArrayList<AccountResource>();
        for (Account acnt : accountService.findAllAccounts())
            accountResList.add(this.accountResourceAssembler.toResource(acnt));
        return new ResponseEntity<List<AccountResource>>(accountResList, HttpStatus.OK);
    

/*Use the below method only if you have enabled spring data web Support or otherwise instead of using Account in @PathVariable usr String id or int id depending on what type to id you have in you db*/

    @RequestMapping(value = "userID", method = RequestMethod.GET)
    public ResponseEntity<AccountResource>  getAccountForID(@PathVariable("userID") Account fetchedAccountForId) 
        return new ResponseEntity<AccountResource>(
                this.accountResourceAssembler.toResource(fetchedAccountForId), HttpStatus.OK);
    

启用 Spring Data Web 支持,为代码添加更多功能,例如根据传递的 id 从 DB 自动获取模型数据,就像我们在之前的方法中使用的那样。

现在返回 toResource(Account account) 方法:首先初始化资源对象,然后设置所需的道具,然后使用静态 org.springframework.hateoas 将链接添加到 AccountResorce .mvc.ControllerLinkBuilder.linkTo(..) 方法,然后传入控制器类,从中选择基本 url,然后使用 slash(..) 等构建 url。指定完整路径后,我们使用 rel 方法来指定关系(就像这里我们使用 withSelfRel() 来指定关系是它自己。对于其他关系,我们可以使用 withRel(String relationship) 更具描述性。 所以在我们的 toResource 方法的代码中,我们使用了类似的东西 accountResource.add(ControllerLinkBuilder.linkTo(RegistrationController.class).slash(account.getId()).withSelfRel());

这会将 URL 构建为 /api/public/accounts/userID

现在在邮递员中,如果我们在这个 url 上使用 get http://localhost:8080/api/public/accounts


    "username": "Arif4",
    "firstName": "xyz",
    "lastName": "Arif",
    "emailAddress": "xyz@outlook.com",
    "address": 
      "addressLine1": "xyz",
      "addressLine2": "xyz",
      "addressLine3": "xyz",
      "city": "xyz",
      "state": "xyz",
      "zipcode": "xyz",
      "country": "India"
    ,
    "links": [
      
        "rel": "self",
        "href": "http://localhost:8080/api/public/accounts/5628b95306bf022f33f0c4f7"
      
    ]
  ,
  
    "username": "Arif5",
    "firstName": "xyz",
    "lastName": "Arif",
    "emailAddress": "xyz@gmail.com",
    "address": 
      "addressLine1": "xyz",
      "addressLine2": "xyz",
      "addressLine3": "xyz",
      "city": "xyz",
      "state": "xyz",
      "zipcode": "xyz",
      "country": "India"
    ,
    "links": [
      
        "rel": "self",
        "href": "http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0"
      
    ]
  

单击任何链接并发送获取请求,响应将是 http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0


    "username": "Arif5",
    "firstName": "xyz",
    "lastName": "Arif",
    "emailAddress": "xyz@gmail.com",
    "address": 
      "addressLine1": "xyz",
      "addressLine2": "xyz",
      "addressLine3": "xyz",
      "city": "xyz",
      "state": "xyz",
      "zipcode": "xyz",
      "country": "India"
    ,
    "links": [
      
        "rel": "self",
        "href": "http://localhost:8080/api/public/accounts/5628c04406bf23ea911facc0"
      
    ]
  

【讨论】:

这是不可接受的。太多样板。一句忠告:org.springframework.http.ResponseEntity 生成样板代码。你最终会在你的控制器中随处可见这些 ResponseEntity ,从而使其难以阅读和理解。如果要处理错误(未找到、冲突等)等特殊情况,可以将 HandlerExceptionResolver 添加到 Spring 配置中。因此,在您的代码中,您只需抛出一个特定的异常(例如 NotFoundException)并决定在您的 Handler 中做什么(将 HTTP 状态设置为 404),从而使 Controller 代码更加清晰。 “太多样板代码”lel 它是 Java .. 添加 2000 行 XML,其中一半是自动生成的,现在您可以使用 Java 的大量样板代码。致 OP:我喜欢这种方法,我希望更多人意识到资源(表示)应该与域模型分开,然后您可以轻松推出新版本的“帐户”【参考方案3】:

有关手动创建链接,请参阅spring-hateoas-examples。 如果没有 DTO,最简单的 wai 是 via new Resource,对于 DTO,最简单的 wai 是 extends ResourceSupport

我自定义的 spring-data-rest 托管实体的链接类似于links to root resource:

MyController implements ResourceProcessor<Resource<ManagedEntity>> 

   @Override
   public Resource<Restaurant> process(Resource<ManagedEntity> resource) 
       resource.add(linkTo(methodOn(MyController.class)
           .myMethod(resource.getContent().getId(), ...)).withRel("..."));
       return resource;

对于分页资源

MyController implements ResourceProcessor<PagedResources<Resource<ManagedEntity>>>

问题是当你需要两者时,由于泛型类型擦除,你不能同时扩展这个接口。作为一个黑客,我创建了虚拟ResourceController

【讨论】:

【参考方案4】:

https://docs.spring.io/spring-hateoas/docs/current/reference/html/#reference

    public class PaymentProcessor implements RepresentationModelProcessor<EntityModel<Order>> 
        @Override
        public EntityModel<Order> process(EntityModel<Order> model) 
            model.add(
                    Link.of("/payments/orderId").withRel(LinkRelation.of("payments")) //
                            .expand(model.getContent().getOrderId()));
            return model;
        
    

migrate-to-1.0.changes

ResourceSupport 现在是 RepresentationModel

资源现在是 EntityModel

Resources 现在是 CollectionModel

PagedResources 现在是 PagedModel

【讨论】:

以上是关于如何简单地添加指向 Spring Data REST 实体的链接的主要内容,如果未能解决你的问题,请参考以下文章

如何正确地将资源 (*.res) 文件添加到带有组件的包中?

Spring Data JPA NamedStoredProcedureQuery 多个输出参数

Vue 更新Data数据页面无反应问题

Spring Data JPA:如何优雅地更新模型?

如何在 Spring Boot 中选择性地升级依赖项? (示例案例:Spring Data)

Spring Data JPA Repository:如何有条件地获取子实体