Spring HATEOAS嵌入式资源支持

问题描述 投票:42回答:8

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

有人可以举例说明如何使用Spring HATEOAS来包含嵌入式资源吗?

java spring spring-hateoas
8个回答
37
投票

一定要阅读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在HALResource推出的another answer非常适合。

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

30
投票

Pre 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"
   }
  ]
 }
}

25
投票

这是我们发现的一个小例子。首先我们使用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("[email protected]", "primary"));
Resource home = new Resource(new Email("[email protected]", "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": "[email protected]",
            "type": "primary"
        },
        {
            "email": "[email protected]",
            "type": "home"
        }
    ]
}
}

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

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


5
投票

通常,HATEOAS需要创建一个代表REST输出的POJO并扩展HATEOAS提供的ResourceSupport。可以在不创建额外POJ的情况下直接使用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;
    }
}

3
投票

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

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

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

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

创建实用程序类:

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"
            },...
        ]
    }
}


0
投票

这就是我用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;
    }
}

并创建联系人资源:

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

0
投票

在您的pom中添加此依赖项。检查此链接: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"
    }
}
© www.soinside.com 2019 - 2024. All rights reserved.