Spring HATEOAS embedded resource support

前端 未结 8 868
别那么骄傲
别那么骄傲 2020-12-07 14:12

I want to use the HAL format for my REST API to include embedded resources. I\'m using Spring HATEOAS for my APIs and Spring HATEOAS seems to support embedded resources; how

相关标签:
8条回答
  • 2020-12-07 14:21

    This is how I've built such json with spring-boot-starter-hateoas 2.1.1:

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

    The main class that encapsulates all this fields is

    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 has info about single contact and it's just 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;
        }
    }
    

    And creating 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;
        }
    }
    

    And I'm just calling this in this way from controller:

    return new ResponseEntity<>(ContactsResourceConverter.toResources(simpleContacts, accountId), HttpStatus.OK);
    
    0 讨论(0)
  • 2020-12-07 14:25

    Make sure to read Spring's documentation about HATEOAS, it helps to get the basics.

    In this answer a core developer points out the concept of Resource, Resources and PagedResources, something essential which is is not covered by the documentation.

    It took me some time to understand how it works, so let's step through some examples to make it crystal-clear.

    Returning a Single Resource

    the resource

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

    the controller

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

    the response

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

    Returning Multiple Resources

    Spring HATEOAS comes with embedded support, which is used by Resources to reflect a response with multiple 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);
        }
    

    the response

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

    If you want to change the key productResources you need to annotate your resource:

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

    Returning a Resource with Embedded Resources

    This is when you need to start to pimp Spring. The HALResource introduced by @chris-damour in another answer suits perfectly.

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

    the controller

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

    the response

    {
        "_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" }
                        }
                    }]
                }
            }
        }
    }
    
    0 讨论(0)
  • 2020-12-07 14:25

    Usually HATEOAS requires to create a POJO that represents the REST output and extends HATEOAS provided ResourceSupport. It is possible do this without creating the extra POJO and use the Resource, Resources and Link classes directly as shown in the code below :

    @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;
        }
    }
    
    0 讨论(0)
  • 2020-12-07 14:37

    Spring will provide a builder https://github.com/spring-projects/spring-hateoas/issues/864

    0 讨论(0)
  • 2020-12-07 14:40

    Add this dependency in your pom. Check this link: https://www.baeldung.com/spring-rest-hal

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

    It will change your response like this.

    "_links": {
        "next": {
            "href": "http://localhost:8082/mbill/user/listUser?extra=ok&page=11"
        }
    }
    
    0 讨论(0)
  • 2020-12-07 14:44

    Combining the answers above I've made a much easier approach:

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

    This is a custom utility class (see below). Note:

    • Second argument of resWrapper accepts ... of embeddedRes calls.
    • You may create another method that omits the relation String inside resWrapper.
    • First argument of embeddedRes is Object, so you may also supply an instance of ResourceSupport
    • The result of the expression is of the type that extends Resource<DomainObjClass>. So, it will be processed by all Spring Data REST ResourceProcessor<Resource<DomainObjClass>>. You may create a collection of them and also wrap around new Resources<>().

    Create the utility class:

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

    You only need to include import static package.ResourceWithEmbeddable.* to your service class to use it.

    JSON looks like this:

    {
        "myField1": "1field",
        "myField2": "2field",
        "_embedded": {
            "settings": [
                {
                    "settingName": "mySetting",
                    "value": "1337",
                    "description": "umh"
                },
                {
                    "settingName": "other",
                    "value": "1488",
                    "description": "a"
                },...
            ]
        }
    }
    
    0 讨论(0)
提交回复
热议问题