Je souhaite utiliser le format HAL pour mon REST pour inclure ressources intégrées . J'utilise Spring HATEOAS pour mes API et Spring HATEOAS semble prendre en charge les ressources intégrées; cependant, il n'y a pas de documentation ou d'exemple sur la façon de l'utiliser.
Quelqu'un peut-il fournir un exemple sur la façon d'utiliser Spring HATEOAS pour inclure des ressources intégrées?
Assurez-vous de lire Spring's documentation sur HATEOAS , cela aide à obtenir les bases.
Dans cette réponse un développeur principal souligne le concept de Resource
, Resources
et PagedResources
, quelque chose d'essentiel qui n'est pas couvert par la documentation.
Il m'a fallu un certain temps pour comprendre comment cela fonctionne, alors passons en revue quelques exemples pour le rendre limpide.
la ressource
import org.springframework.hateoas.ResourceSupport;
public class ProductResource extends ResourceSupport{
final String name;
public ProductResource(String name) {
this.name = name;
}
}
le controlle
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);
}
}
la réponse
{
"name": "Apfelstrudel",
"_links": {
"self": { "href": "http://example.com/products/1" }
}
}
Spring HATEOAS est fourni avec un support intégré, qui est utilisé par Resources
pour refléter une réponse avec plusieurs ressources.
@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);
}
la réponse
{
"_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" }
}
}]
}
}
Si vous souhaitez modifier la clé productResources
, vous devez annoter votre ressource:
@Relation(collectionRelation = "items")
class ProductResource ...
C'est à ce moment que vous devez commencer à pimper le printemps. Le HALResource
introduit par @ chris-damour dans ne autre réponse convient parfaitement.
public class OrderResource extends HalResource {
final float totalPrice;
public OrderResource(float totalPrice) {
this.totalPrice = totalPrice;
}
}
le controlle
@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);
}
la réponse
{
"_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" }
}
}]
}
}
}
}
Pré HATEOAS 1.0.0M1: je n'ai pas trouvé de moyen officiel de le faire ... voici ce que nous avons fait
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);
}
}
puis fait étendre nos ressources HALResource
MISE À JOUR: dans HATEOAS 1.0.0M1, EntityModel (et vraiment tout ce qui étend RepresentationalModel), cela est désormais pris en charge nativement tant que la ressource intégrée est exposée via un getContent (ou cependant vous faites jackson sérialiser une propriété de contenu). comme:
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);
tu auras
{
_embedded: {
purchased: {
name: "Product2a"
},
all: [
{
name: "Product1a"
},
{
name: "Product1b"
}
]
}
}
voici un petit exemple de ce que nous avons trouvé. Tout d'abord, nous utilisons spring-hateoas-0.16
Imagerie que nous avons GET /profile
qui devrait renvoyer le profil utilisateur avec une liste de courriels intégrée.
Nous avons une ressource email.
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@Relation(value = "email", collectionRelation = "emails")
public class EmailResource {
private final String email;
private final String type;
}
deux e-mails que nous voulons intégrer dans la réponse du profil
Resource primary = new Resource(new Email("[email protected]", "primary"));
Resource home = new Resource(new Email("[email protected]", "home"));
Pour indiquer que ces ressources sont intégrées, nous avons besoin d'une instance d'EmbeddedWrappers:
import org.springframework.hateoas.core.EmbeddedWrappers
EmbeddedWrappers wrappers = new EmbeddedWrappers(true);
Avec l'aide de wrappers
, nous pouvons créer une instance de EmbeddedWrapper
pour chaque e-mail et les mettre dans une liste.
List<EmbeddedWrapper> embeddeds = Arrays.asList(wrappers.wrap(primary), wrappers.wrap(home))
La seule chose qui reste à faire est de construire notre ressource de profil avec ces intégrations. Dans l'exemple ci-dessous, j'utilise lombok pour raccourcir le code.
@Data
@Relation(value = "profile")
public class ProfileResource {
private final String firstName;
private final String lastName;
@JsonUnwrapped
private final Resources<EmbeddedWrapper> embeddeds;
}
Gardez à l'esprit l'annotation @JsonUnwrapped
dans le champ intégré
Et nous sommes prêts à renvoyer tout cela du contrôleur
...
Resources<EmbeddedWrapper> embeddedEmails = new Resources(embeddeds, linkTo(EmailAddressController.class).withSelfRel());
return ResponseEntity.ok(new Resource(new ProfileResource("Thomas", "Anderson", embeddedEmails), linkTo(ProfileController.class).withSelfRel()));
}
Maintenant, dans la réponse, nous aurons
{
"firstName": "Thomas",
"lastName": "Anderson",
"_links": {
"self": {
"href": "http://localhost:8080/profile"
}
},
"_embedded": {
"emails": [
{
"email": "[email protected]",
"type": "primary"
},
{
"email": "[email protected]",
"type": "home"
}
]
}
}
Partie intéressante dans l'utilisation de Resources<EmbeddedWrapper> embeddeds
est que vous pouvez y mettre différentes ressources et il les regroupera automatiquement par relations. Pour cela, nous utilisons l'annotation @Relation
de org.springframework.hateoas.core
paquet.
Il y a aussi un bon article sur les ressources intégrées dans HAL
Habituellement, HATEOAS nécessite de créer un POJO qui représente la sortie REST et étend ResourceSupport fourni par HATEOAS. Il est possible de le faire sans créer le POJO supplémentaire et d'utiliser les classes Resource, Resources et Link directement comme indiqué dans le code ci-dessous:
@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;
}
}
En combinant les réponses ci-dessus, j'ai rendu l'approche beaucoup plus facile:
return resWrapper(domainObj, embeddedRes(domainObj.getSettings(), "settings"))
Il s'agit d'une classe d'utilitaires personnalisée (voir ci-dessous). Remarque:
resWrapper
accepte ...
Des appels de embeddedRes
.resWrapper
.embeddedRes
est Object
, vous pouvez donc également fournir une instance de ResourceSupport
Resource<DomainObjClass>
. Ainsi, elles seront traitées par toutes les données Spring REST ResourceProcessor<Resource<DomainObjClass>>
. Vous pouvez en créer une collection et en faire un tour autour de new Resources<>()
.Créez la classe d'utilité:
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);
}
}
Il vous suffit d'inclure import static package.ResourceWithEmbeddable.*
À votre classe de service pour l'utiliser.
JSON ressemble à ceci:
{
"myField1": "1field",
"myField2": "2field",
"_embedded": {
"settings": [
{
"settingName": "mySetting",
"value": "1337",
"description": "umh"
},
{
"settingName": "other",
"value": "1488",
"description": "a"
},...
]
}
}
Voici comment j'ai construit un tel json avec 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"
}
}
}
La classe principale qui encapsule tous ces champs est
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 a des informations sur le contact unique et c'est juste 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;
}
}
Et la création de 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;
}
}
Et je l'appelle simplement de cette façon depuis le contrôleur:
return new ResponseEntity<>(ContactsResourceConverter.toResources(simpleContacts, accountId), HttpStatus.OK);
Ajoutez cette dépendance dans votre pom. Vérifiez ce lien: https://www.baeldung.com/spring-rest-hal
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-rest-hal-browser</artifactId>
</dependency>
Cela changera votre réponse comme ceci.
"_links": {
"next": {
"href": "http://localhost:8082/mbill/user/listUser?extra=ok&page=11"
}
}
Spring fournira un constructeur https://github.com/spring-projects/spring-hateoas/issues/864