Spring Поддержка встроенных ресурсов HATEOAS
Я хочу использовать формат HAL для моего REST API для включения встроенных ресурсов. Я использую Spring HATEOAS для своих API и Spring HATEOAS, похоже, поддерживает встроенные ресурсы; однако нет документации или примера о том, как ее использовать.
Может ли кто-нибудь предоставить пример использования Spring HATEOAS для включения встроенных ресурсов?
Ответы
Ответ 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"
},...
]
}
}
Ответ 6
Spring предоставит застройщика https://github.com/spring-projects/spring-hateoas/issues/864