【问题标题】:How can I simply add a link to a Spring Data REST Entity如何简单地添加指向 Spring Data REST 实体的链接
【发布时间】: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 存储库中声明的方法的链接列表。我将如何处理这个问题?谢谢。

【问题讨论】:

    标签: spring entity-framework jpa spring-data-rest 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

      【讨论】:

        猜你喜欢
        • 2015-03-23
        • 2015-11-13
        • 2014-06-01
        • 1970-01-01
        • 2015-03-14
        • 2014-12-10
        • 1970-01-01
        • 2015-11-29
        • 1970-01-01
        相关资源
        最近更新 更多