Java Spring HATEOAS嵌入式资源支持

Java Spring HATEOAS嵌入式资源支持,java,spring,spring-hateoas,Java,Spring,Spring Hateoas,我想使用HAL格式来包含RESTAPI。我在API中使用SpringHateoas,SpringHateoas似乎支持嵌入式资源;但是,没有关于如何使用此功能的文档或示例 有人能举例说明如何使用Spring HATEOAS来包含嵌入式资源吗?Pre-HATEOAS 1.0.0M1:我找不到一个正式的方法来实现这一点……下面是我们所做的 public abstract class HALResource extends ResourceSupport { private final Ma

我想使用HAL格式来包含RESTAPI。我在API中使用SpringHateoas,SpringHateoas似乎支持嵌入式资源;但是,没有关于如何使用此功能的文档或示例


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

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

下面是我们发现的一个小例子。 首先,我们使用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"));
{
    "name": "Apfelstrudel",
    "_links": {
        "self": { "href": "http://example.com/products/1" }
    }
}
{
    "_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" }
            }
        }]
    }
}
{
    "_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" }
                    }
                }]
            }
        }
    }
}
为了表明这些资源是嵌入式的,我们需要一个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))
现在在回答中,我们将

{
"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"
        }
    ]
}
}
使用嵌入的
资源
的有趣之处在于,您可以将不同的资源放入其中,它将根据关系自动对它们进行分组。为此,我们使用
org.springframework.hateoas.core
包中的注释
@Relation


此外,HAL中还有一个关于嵌入式资源的问题,HATEOAS通常需要创建一个表示REST输出的POJO,并扩展HATEOAS提供的资源支持。可以在不创建额外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;
    }
}
@RestController
类客户控制器{
列出客户名单;
公共CustomerController(){
客户=新的LinkedList();
添加(新客户(1,“彼得”,“测试”));
添加(新客户(2,“Peter”,“Test2”));
}
@RequestMapping(value=“/customers”,method=RequestMethod.GET,products=“application/hal+json”)
公共资源{
列表链接=新建链接列表();
add(linkTo(methodOn(CustomerController.class).getCustomers()).withSelfRel());
列表资源=customerToResource(customers.toArray(新客户[0]);
返回新资源(资源、链接);
}
@RequestMapping(value=“/customer/{id}”,method=RequestMethod.GET,products=“application/hal+json”)
公共资源getCustomer(@PathVariable int-id){
Link=linkTo(methodOn(CustomerController.class).getCustomer(id)).withSelfRel();
可选customer=customers.stream().filter(customer1->customer1.getId()==id.findFirst();
列表资源=customerToResource(customer.get());
返回新资源(资源、链接);
}
私有列表客户来源(客户…客户){
列表资源=新的ArrayList(customers.length);
用于(客户:客户){
Link selfLink=linkTo(methodOn(CustomerController.class).getCustomer(customer.getId()).withSelfRel();
添加(新资源(客户、自链接));
}
归还资源;
}
}

确保阅读Spring的,它有助于获得基本知识

核心开发人员指出了
资源
资源
页面资源
的概念,这是文档中未涉及的基本概念

我花了一些时间来理解它是如何工作的,所以让我们通过一些示例来清楚地了解它

返回单个资源 资源

import org.springframework.hateoas.ResourceSupport;


public class ProductResource extends ResourceSupport{
    final String name;

    public ProductResource(String name) {
        this.name = name;
    }
}
控制器

...
Resources<EmbeddedWrapper> embeddedEmails = new Resources(embeddeds, linkTo(EmailAddressController.class).withSelfRel());
return ResponseEntity.ok(new Resource(new ProfileResource("Thomas", "Anderson", embeddedEmails), linkTo(ProfileController.class).withSelfRel()));
}
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);
    }
}
    @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);
    }
返回多个资源 SpringHateOAS提供了嵌入式支持,
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);
    }
如果要更改键
productResources
,则需要为资源添加注释:

@Relation(collectionRelation = "items")
class ProductResource ...
返回包含嵌入式资源的资源 这是你需要开始拉皮条的时候了。由穿着西装的@chris damour介绍的
HALResource

public class OrderResource extends HalResource {
    final float totalPrice;

    public OrderResource(float totalPrice) {
        this.totalPrice = totalPrice;
    }
}
控制器

...
Resources<EmbeddedWrapper> embeddedEmails = new Resources(embeddeds, linkTo(EmailAddressController.class).withSelfRel());
return ResponseEntity.ok(new Resource(new ProfileResource("Thomas", "Anderson", embeddedEmails), linkTo(ProfileController.class).withSelfRel()));
}
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);
    }
}
    @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);
    }

结合以上答案,我提出了一个更简单的方法:

return resWrapper(domainObj, embeddedRes(domainObj.getSettings(), "settings"))
这是一个自定义实用程序类(请参见下文)。注:

  • resWrapper
    的第二个参数接受
    embeddedRes
    调用的
    ..
  • 您可以创建另一个方法,在
    resWrapper
    中省略关系字符串
  • embeddedRes
    的第一个参数是
    Object
    ,因此您还可以提供
    ResourceSupport
  • 表达式的结果属于扩展
    资源的类型。因此,它将被所有Spring数据REST
    ResourceProcessor
    处理。您可以创建它们的集合,还可以环绕
    新资源()
创建实用程序类:

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

Spring将提供一个构建器

这就是我如何使用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;
    }
}
和创建联系人资源:

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;
    }
}
公共类ContactsResourceConverter{
公共静态联系人资源到资源(页面simpleContacts,长帐户ID){
List embeddeds=simpleContacts.stream().map(联系人->{
Link self=linkTo(methodOn(AccountController.class).getContactById(accountId,contact.getId())。
with selfrel();
返回新资源(联系人、自身);
}
).collect(Collectors.toList());
List listOfLinks=new ArrayList();
//自链接
Link selfLink=linkTo(methodOn(AccountController.class).getContactsForAccount(
帐户ID,
simpleContacts.getPageable().getPageSize(),
simpleContacts.getPageable().getPageNumber()+1))//+1,因为第一个索引为0
.with selfrel();
添加(自链接);
…另一个链接
资源=新资源(embe)
@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);
<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"
    }
}