Подтвердить что ты не робот

Spring Поддержка встроенных ресурсов HATEOAS

Я хочу использовать формат HAL для моего REST API для включения встроенных ресурсов. Я использую Spring HATEOAS для своих API и Spring HATEOAS, похоже, поддерживает встроенные ресурсы; однако нет документации или примера о том, как ее использовать.

Может ли кто-нибудь предоставить пример использования Spring HATEOAS для включения встроенных ресурсов?

4b9b3361

Ответ 1

Обязательно прочитайте Spring документацию о HATEOAS, это поможет понять основы.

В этом ответе основной разработчик указывает на концепцию Resource, Resources и PagedResources, чего-то существенного, что не PagedResources в документации.

Мне потребовалось некоторое время, чтобы понять, как это работает, поэтому давайте рассмотрим несколько примеров, чтобы сделать его кристально ясным.

Возврат одного ресурса

ресурс

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 ...

Возврат ресурса со встроенными ресурсами

Это когда вам нужно начать сутенерство Spring. HALResource представленный @chris-damour в другом ответе, идеально подходит.

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

Ответ 2

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 (и, действительно, все, что расширяет PresentationalModel) это изначально поддерживается, пока встроенный ресурс предоставляется через getContent (или, тем не менее, вы заставляете Джексона сериализовать свойство содержимого). лайк:

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

Ответ 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("[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

Ответ 4

Обычно HATEOAS требует создания POJO, которое представляет выход REST и расширяет HATEOAS, предоставляемый ResourceSupport. Это можно сделать без создания дополнительного POJO и напрямую использовать классы ресурсов, ресурсов и ссылок, как показано в приведенном ниже коде:

@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.
  • Вы можете создать другой метод, который опускает отношение String внутри 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"
            },...
        ]
    }
}