【问题标题】:Spring HATEOAS embedded resource supportSpring HATEOAS 嵌入式资源支持
【发布时间】:2014-11-09 14:36:08
【问题描述】:

我想为我的 REST API 使用 HAL 格式以包含 embedded resources。我将 Spring HATEOAS 用于我的 API,而 Spring HATEOAS 似乎支持嵌入式资源;但是,没有关于如何使用它的文档或示例。

谁能提供一个如何使用 Spring HATEOAS 来包含嵌入式资源的示例?

【问题讨论】:

    标签: java spring spring-hateoas


    【解决方案1】:

    请务必阅读 Spring 的 documentation about HATEOAS,它有助于了解基础知识。

    In this answer 一位核心开发人员指出了ResourceResourcesPagedResources 的概念,这是文档中未涵盖的基本内容。

    我花了一些时间来理解它是如何工作的,所以让我们通过一些例子来说明它。

    返回单个资源

    资源

    import org.springframework.hateoas.ResourceSupport;
    
    
    public class ProductResource extends ResourceSupport{
        final String name;
    
        public ProductResource(String name) {
            this.name = name;
        }
    }
    

    控制器

    import org.springframework.hateoas.Link;
    import org.springframework.hateoas.Resource;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class MyController {
        @RequestMapping("products/{id}", method = RequestMethod.GET)
        ResponseEntity<Resource<ProductResource>> get(@PathVariable Long id) {
            ProductResource productResource = new ProductResource("Apfelstrudel");
            Resource<ProductResource> resource = new Resource<>(productResource, new Link("http://example.com/products/1"));
            return ResponseEntity.ok(resource);
        }
    }
    

    回复

    {
        "name": "Apfelstrudel",
        "_links": {
            "self": { "href": "http://example.com/products/1" }
        }
    }
    

    返回多个资源

    Spring HATEOAS 带有嵌入式支持,Resources 使用它来反映具有多个资源的响应。

        @RequestMapping("products/", method = RequestMethod.GET)
        ResponseEntity<Resources<Resource<ProductResource>>> getAll() {
            ProductResource p1 = new ProductResource("Apfelstrudel");
            ProductResource p2 = new ProductResource("Schnitzel");
    
            Resource<ProductResource> r1 = new Resource<>(p1, new Link("http://example.com/products/1"));
            Resource<ProductResource> r2 = new Resource<>(p2, new Link("http://example.com/products/2"));
    
            Link link = new Link("http://example.com/products/");
            Resources<Resource<ProductResource>> resources = new Resources<>(Arrays.asList(r1, r2), link);
    
            return ResponseEntity.ok(resources);
        }
    

    回复

    {
        "_links": {
            "self": { "href": "http://example.com/products/" }
        },
        "_embedded": {
            "productResources": [{
                "name": "Apfelstrudel",
                "_links": {
                    "self": { "href": "http://example.com/products/1" }
                }, {
                "name": "Schnitzel",
                "_links": {
                    "self": { "href": "http://example.com/products/2" }
                }
            }]
        }
    }
    

    如果您想更改密钥productResources,您需要注释您的资源:

    @Relation(collectionRelation = "items")
    class ProductResource ...
    

    返回具有嵌入式资源的资源

    这是你需要开始拉皮条春天的时候。 @chris-damour 在another answer 中介绍的HALResource 非常适合。

    public class OrderResource extends HalResource {
        final float totalPrice;
    
        public OrderResource(float totalPrice) {
            this.totalPrice = totalPrice;
        }
    }
    

    控制器

        @RequestMapping(name = "orders/{id}", method = RequestMethod.GET)
        ResponseEntity<OrderResource> getOrder(@PathVariable Long id) {
            ProductResource p1 = new ProductResource("Apfelstrudel");
            ProductResource p2 = new ProductResource("Schnitzel");
    
            Resource<ProductResource> r1 = new Resource<>(p1, new Link("http://example.com/products/1"));
            Resource<ProductResource> r2 = new Resource<>(p2, new Link("http://example.com/products/2"));
            Link link = new Link("http://example.com/order/1/products/");
    
            OrderResource resource = new OrderResource(12.34f);
            resource.add(new Link("http://example.com/orders/1"));
    
            resource.embed("products", new Resources<>(Arrays.asList(r1, r2), link));
    
            return ResponseEntity.ok(resource);
        }
    

    回复

    {
        "_links": {
            "self": { "href": "http://example.com/products/1" }
        },
        "totalPrice": 12.34,
        "_embedded": {
            "products":     {
                "_links": {
                    "self": { "href": "http://example.com/orders/1/products/" }
                },
                "_embedded": {
                    "items": [{
                        "name": "Apfelstrudel",
                        "_links": {
                            "self": { "href": "http://example.com/products/1" }
                        }, {
                        "name": "Schnitzel",
                        "_links": {
                            "self": { "href": "http://example.com/products/2" }
                        }
                    }]
                }
            }
        }
    }
    

    【讨论】:

    • @Glide,您可以考虑接受这个答案。很好的回应,@linqu!
    • 你有 resource.embed("products", new Resources&lt;&gt;(Arrays.asList(r1, r2), link)); 。这个embed 方法来自哪里?是不是只是重命名了 HALResource embedResource 方法?
    • @GreenAsJade 类 OrderResource 扩展了 HalResource,这就是 embed 方法的来源,你是对的,这令人困惑:我将 embed 重命名为 embedResource。
    • 谢谢。你的解决方案让我克服了障碍:) 最难的是接受我们必须这样做! ;)
    【解决方案2】:

    HATEOAS 1.0.0M1 之前:我找不到正式的方法来做到这一点......这就是我们所做的

    public abstract class HALResource extends ResourceSupport {
    
        private final Map<String, ResourceSupport> embedded = new HashMap<String, ResourceSupport>();
    
        @JsonInclude(Include.NON_EMPTY)
        @JsonProperty("_embedded")
        public Map<String, ResourceSupport> getEmbeddedResources() {
            return embedded;
        }
    
        public void embedResource(String relationship, ResourceSupport resource) {
    
            embedded.put(relationship, resource);
        }  
    }
    

    然后让我们的资源扩展 HALResource

    更新:在 HATEOAS 1.0.0M1 中,EntityModel(以及实际上任何扩展 RepresentationalModel)现在原生支持,只要嵌入式资源通过 getContent 公开(或者您让 jackson 序列化内容属性)。喜欢:

        public class Result extends RepresentationalModel<Result> {
            private final List<Object> content;
    
            public Result(
    
                List<Object> content
            ){
    
                this.content = content;
            }
    
            public List<Object> getContent() {
                return content;
            }
        };
    
        EmbeddedWrappers wrappers = new EmbeddedWrappers(false);
        List<Object> elements = new ArrayList<>();
    
        elements.add(wrappers.wrap(new Product("Product1a"), LinkRelation.of("all")));
        elements.add(wrappers.wrap(new Product("Product2a"), LinkRelation.of("purchased")));
        elements.add(wrappers.wrap(new Product("Product1b"), LinkRelation.of("all")));
    
        return new Result(elements);
    
    

    你会得到

    {
     _embedded: {
       purchased: {
        name: "Product2a"
       },
      all: [
       {
        name: "Product1a"
       },
       {
        name: "Product1b"
       }
      ]
     }
    }
    

    【讨论】:

    • 同样,我们找不到任何内置的东西来支持这一点,所以我们在我们的资源上添加了一个 _embedded 属性
    • 这也是我最终做的,所以我赞成答案。但希望有一个正式的方式,所以不会标记答案被接受:(
    • 这也是我最终使用的,稍作修改。我的地图是Map&lt;String, List&lt;ResourceSupport&gt;&gt; 类型的,因为一个 rel 可以有一个特定资源的多个实例(即,基本上代表一个资源集合)。目前的解决方案没有考虑到这一点。
    • 这样做的一个问题是您必须为要返回的每个实体创建一个资源类。
    • 请告诉我,多年后不再是这种情况了,为最基本的标准 HAL 事情做定制的东西!为什么 spring HATEOAS 假装它可以管理 HAL 并将其设置为默认值,如果它甚至不能自动执行此操作?
    【解决方案3】:

    这是我们发现的一个小例子。 首先我们使用 spring-hateoas-0.16

    我们有 GET /profile 的图像,它应该返回带有嵌入电子邮件列表的用户配置文件。

    我们有电子邮件资源。

    @Data
    @JsonIgnoreProperties(ignoreUnknown = true)
    @Relation(value = "email", collectionRelation = "emails")
    public class EmailResource {
        private final String email;
        private final String type;
    }
    

    我们想要嵌入到个人资料回复中的两封电子邮件

    Resource primary = new Resource(new Email("neo@matrix.net", "primary"));
    Resource home = new Resource(new Email("t.anderson@matrix.net", "home"));
    

    为了表明这些资源是嵌入的,我们需要一个 EmbeddedWrappers 的实例:

    import org.springframework.hateoas.core.EmbeddedWrappers
    EmbeddedWrappers wrappers = new EmbeddedWrappers(true);
    

    wrappers 的帮助下,我们可以为每封电子邮件创建EmbeddedWrapper 实例并将它们放入一个列表中。

    List<EmbeddedWrapper> embeddeds = Arrays.asList(wrappers.wrap(primary), wrappers.wrap(home))
    

    剩下要做的就是用这些嵌入构建我们的配置文件资源。在下面的示例中,我使用 lombok 来缩短代码。

    @Data
    @Relation(value = "profile")
    public class ProfileResource {
        private final String firstName;
        private final String lastName;
        @JsonUnwrapped
        private final Resources<EmbeddedWrapper> embeddeds;
    }
    

    记住在嵌入字段上注释@JsonUnwrapped

    我们已经准备好从控制器返回所有这些

    ...
    Resources<EmbeddedWrapper> embeddedEmails = new Resources(embeddeds, linkTo(EmailAddressController.class).withSelfRel());
    return ResponseEntity.ok(new Resource(new ProfileResource("Thomas", "Anderson", embeddedEmails), linkTo(ProfileController.class).withSelfRel()));
    }
    

    现在我们将得到响应

    {
    "firstName": "Thomas",
    "lastName": "Anderson",
    "_links": {
        "self": {
            "href": "http://localhost:8080/profile"
        }
    },
    "_embedded": {
        "emails": [
            {
                "email": "neo@matrix.net",
                "type": "primary"
            },
            {
                "email": "t.anderson@matrix.net",
                "type": "home"
            }
        ]
    }
    }
    

    使用Resources&lt;EmbeddedWrapper&gt; embeddeds 的有趣之处在于您可以将不同的资源放入其中,它会自动按关系对它们进行分组。为此,我们使用 org.springframework.hateoas.core 包中的注解 @Relation

    还有一个关于 HAL 中嵌入资源的good article

    【讨论】:

    • 谢谢!您在哪里找到有关如何使用 EmbeddedWrapper 的文档?
    【解决方案4】:

    通常 HATEOAS 需要创建一个 POJO 来表示 REST 输出并扩展 HATEOAS 提供的 ResourceSupport。可以在不创建额外 POJO 的情况下执行此操作,并直接使用 Resource、Resources 和 Link 类,如下面的代码所示:

    @RestController
    class CustomerController {
    
        List<Customer> customers;
    
        public CustomerController() {
            customers = new LinkedList<>();
            customers.add(new Customer(1, "Peter", "Test"));
            customers.add(new Customer(2, "Peter", "Test2"));
        }
    
        @RequestMapping(value = "/customers", method = RequestMethod.GET, produces = "application/hal+json")
        public Resources<Resource> getCustomers() {
    
            List<Link> links = new LinkedList<>();
            links.add(linkTo(methodOn(CustomerController.class).getCustomers()).withSelfRel());
            List<Resource> resources = customerToResource(customers.toArray(new Customer[0]));
    
            return new Resources<>(resources, links);
    
        }
    
        @RequestMapping(value = "/customer/{id}", method = RequestMethod.GET, produces = "application/hal+json")
        public Resources<Resource> getCustomer(@PathVariable int id) {
    
            Link link = linkTo(methodOn(CustomerController.class).getCustomer(id)).withSelfRel();
    
            Optional<Customer> customer = customers.stream().filter(customer1 -> customer1.getId() == id).findFirst();
    
            List<Resource> resources = customerToResource(customer.get());
    
            return new Resources<Resource>(resources, link);
    
        }
    
        private List<Resource> customerToResource(Customer... customers) {
    
            List<Resource> resources = new ArrayList<>(customers.length);
    
            for (Customer customer : customers) {
                Link selfLink = linkTo(methodOn(CustomerController.class).getCustomer(customer.getId())).withSelfRel();
                resources.add(new Resource<Customer>(customer, selfLink));
            }
    
            return resources;
        }
    }
    

    【讨论】:

      【解决方案5】:

      结合上面的答案,我做了一个更简单的方法:

      return resWrapper(domainObj, embeddedRes(domainObj.getSettings(), "settings"))
      

      这是一个自定义实用程序类(见下文)。注意:

      • resWrapper 的第二个参数接受 ...embeddedRes 调用。
      • 您可以创建另一个忽略resWrapper 中的关系字符串的方法。
      • embeddedRes 的第一个参数是Object,因此您也可以提供ResourceSupport 的实例
      • 表达式的结果是扩展Resource&lt;DomainObjClass&gt; 的类型。因此,它将由所有 Spring Data REST ResourceProcessor&lt;Resource&lt;DomainObjClass&gt;&gt; 处理。您可以创建它们的集合并环绕new Resources&lt;&gt;()

      创建实用程序类:

      import com.fasterxml.jackson.annotation.JsonUnwrapped;
      import java.util.Arrays;
      import org.springframework.hateoas.Link;
      import org.springframework.hateoas.Resource;
      import org.springframework.hateoas.Resources;
      import org.springframework.hateoas.core.EmbeddedWrapper;
      import org.springframework.hateoas.core.EmbeddedWrappers;
      
      public class ResourceWithEmbeddable<T> extends Resource<T> {
      
          @SuppressWarnings("FieldCanBeLocal")
          @JsonUnwrapped
          private Resources<EmbeddedWrapper> wrappers;
      
          private ResourceWithEmbeddable(final T content, final Iterable<EmbeddedWrapper> wrappers, final Link... links) {
      
              super(content, links);
              this.wrappers = new Resources<>(wrappers);
          }
      
      
          public static <T> ResourceWithEmbeddable<T> resWrapper(final T content,
                                                                 final EmbeddedWrapper... wrappers) {
      
              return new ResourceWithEmbeddable<>(content, Arrays.asList(wrappers));
      
          }
      
          public static EmbeddedWrapper embeddedRes(final Object source, final String rel) {
              return new EmbeddedWrappers(false).wrap(source, rel);
          }
      }
      

      您只需将import static package.ResourceWithEmbeddable.* 包含到您的服务类中即可使用它。

      JSON 看起来像这样:

      {
          "myField1": "1field",
          "myField2": "2field",
          "_embedded": {
              "settings": [
                  {
                      "settingName": "mySetting",
                      "value": "1337",
                      "description": "umh"
                  },
                  {
                      "settingName": "other",
                      "value": "1488",
                      "description": "a"
                  },...
              ]
          }
      }
      

      【讨论】:

      • 能否再添加ResourceWithEmbeddable类的用法示例?
      • @Speise 看看这个问题,Spring 会提供合适的构建器github.com/spring-projects/spring-hateoas/issues/864
      • 我在最近的项目中没有使用 hatoas,所以我手上没有这段代码,也不想在 Spring 已经做某事的时候花时间。但是我从生产代码中复制粘贴了它,所以它应该可以工作。上下文是 Spring Data REST
      【解决方案6】:

      【讨论】:

        【解决方案7】:

        这就是我使用 spring-boot-starter-hateoas 2.1.1 构建此类 json 的方式:

        {
            "total": 2,
            "count": 2,
            "_embedded": {
                "contacts": [
                    {
                        "id": "1-1CW-303",
                        "role": "ASP",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/accounts/2700098669/contacts/1-1CW-303"
                            }
                        }
                    },
                    {
                        "id": "1-1D0-267",
                        "role": "HSP",
                        "_links": {
                            "self": {
                                "href": "http://localhost:8080/accounts/2700098669/contacts/1-1D0-267"
                            }
                        }
                    }
                ]
            },
            "_links": {
                "self": {
                    "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
                },
                "first": {
                    "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
                },
                "last": {
                    "href": "http://localhost:8080/accounts/2700098669/contacts?limit=2&page=1"
                }
            }
        }
        

        封装所有这些字段的主类是

        public class ContactsResource extends ResourceSupport{
            private long count;
            private long total;
            private final Resources<Resource<SimpleContact>> contacts;
        
            public long getTotal() {
                return total;
            }
        
            public ContactsResource(long total, long count, Resources<Resource<SimpleContact>> contacts){
                this.contacts = contacts;
                this.total = total;
                this.count = count;
            }
        
            public long getCount() {
                return count;
            }
        
            @JsonUnwrapped
            public Resources<Resource<SimpleContact>> getContacts() {
                return contacts;
            }
        }
        

        SimpleContact 有关于单个联系人的信息,它只是 pojo

        @Relation(value = "contact", collectionRelation = "contacts")
        public class SimpleContact {
            private String id;
            private String role;
        
            public String getId() {
                return id;
            }
        
            public SimpleContact id(String id) {
                this.id = id;
                return this;
            }
        
            public String getRole() {
                return role;
            }
        
            public SimpleContact role(String role) {
                this.role = role;
                return this;
            }
        }
        

        并创建 ContactsResource:

        public class ContactsResourceConverter {
        
            public static ContactsResource toResources(Page<SimpleContact> simpleContacts, Long accountId){
        
                List<Resource<SimpleContact>> embeddeds = simpleContacts.stream().map(contact -> {
                    Link self = linkTo(methodOn(AccountController.class).getContactById(accountId, contact.getId())).
                            withSelfRel();
                    return new Resource<>(contact, self);
                }
                ).collect(Collectors.toList());
        
                List<Link> listOfLinks = new ArrayList<>();
                //self link
                Link selfLink = linkTo(methodOn(AccountController.class).getContactsForAccount(
                        accountId,
                        simpleContacts.getPageable().getPageSize(),
                        simpleContacts.getPageable().getPageNumber() + 1)) // +1 because of 0 first index
                        .withSelfRel();
                listOfLinks.add(selfLink);
        
                ... another links           
        
                Resources<Resource<SimpleContact>> resources = new Resources<>(embeddeds);
                ContactsResource contactsResource = new ContactsResource(simpleContacts.getTotalElements(), simpleContacts.getNumberOfElements(), resources);
                contactsResource.add(listOfLinks);
        
                return contactsResource;
            }
        }
        

        我只是从控制器以这种方式调用它:

        return new ResponseEntity<>(ContactsResourceConverter.toResources(simpleContacts, accountId), HttpStatus.OK);
        

        【讨论】:

          【解决方案8】:

          在你的 pom.xml 中添加这个依赖。 检查此链接: https://www.baeldung.com/spring-rest-hal

          <dependency>
                  <groupId>org.springframework.data</groupId>
                  <artifactId>spring-data-rest-hal-browser</artifactId>
          </dependency>
          

          它会像这样改变你的反应。

          "_links": {
              "next": {
                  "href": "http://localhost:8082/mbill/user/listUser?extra=ok&page=11"
              }
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2015-12-03
            • 2021-07-17
            • 2023-03-22
            • 2015-07-28
            • 2011-10-30
            • 1970-01-01
            • 2016-11-05
            相关资源
            最近更新 更多