HATEOAS per un servizio di RIPOSO primaverile

REST Top

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO

1. Panoramica

Questo articolo si concentrerà sull'implementazione della rilevabilità in un servizio REST di primavera e sul rispetto del vincolo HATEOAS.

Questo articolo si concentra su Spring MVC. Il nostro articolo An Intro to Spring HATEOAS descrive come utilizzare HATEOAS in Spring Boot.

2. Disaccoppiamento della rilevabilità tramite eventi

La rilevabilità come aspetto o problema separato del livello web dovrebbe essere disaccoppiata dal controller che gestisce la richiesta HTTP. A tal fine, il Controller attiverà eventi per tutte le azioni che richiedono una manipolazione aggiuntiva della risposta.

Per prima cosa, creiamo gli eventi:

public class SingleResourceRetrieved extends ApplicationEvent { private HttpServletResponse response; public SingleResourceRetrieved(Object source, HttpServletResponse response) { super(source); this.response = response; } public HttpServletResponse getResponse() { return response; } } public class ResourceCreated extends ApplicationEvent { private HttpServletResponse response; private long idOfNewResource; public ResourceCreated(Object source, HttpServletResponse response, long idOfNewResource) { super(source); this.response = response; this.idOfNewResource = idOfNewResource; } public HttpServletResponse getResponse() { return response; } public long getIdOfNewResource() { return idOfNewResource; } }

Quindi, il controller, con 2 semplici operazioni: trova per id e crea :

@RestController @RequestMapping(value = "/foos") public class FooController { @Autowired private ApplicationEventPublisher eventPublisher; @Autowired private IFooService service; @GetMapping(value = "foos/{id}") public Foo findById(@PathVariable("id") Long id, HttpServletResponse response) { Foo resourceById = Preconditions.checkNotNull(service.findOne(id)); eventPublisher.publishEvent(new SingleResourceRetrieved(this, response)); return resourceById; } @PostMapping @ResponseStatus(HttpStatus.CREATED) public void create(@RequestBody Foo resource, HttpServletResponse response) { Preconditions.checkNotNull(resource); Long newId = service.create(resource).getId(); eventPublisher.publishEvent(new ResourceCreated(this, response, newId)); } }

Possiamo quindi gestire questi eventi con un numero qualsiasi di listener disaccoppiati. Ognuno di questi può concentrarsi sul proprio caso particolare e aiutare a soddisfare il vincolo generale di HATEOAS.

I listener dovrebbero essere gli ultimi oggetti nello stack di chiamate e non è necessario alcun accesso diretto ad essi; in quanto tali non sono pubblici.

3. Rendere rilevabile l'URI di una risorsa appena creata

Come discusso nel post precedente su HATEOAS, l'operazione di creazione di una nuova risorsa dovrebbe restituire l'URI di quella risorsa nell'intestazione HTTP Location della risposta.

Lo gestiremo utilizzando un listener:

@Component class ResourceCreatedDiscoverabilityListener implements ApplicationListener{ @Override public void onApplicationEvent(ResourceCreated resourceCreatedEvent){ Preconditions.checkNotNull(resourceCreatedEvent); HttpServletResponse response = resourceCreatedEvent.getResponse(); long idOfNewResource = resourceCreatedEvent.getIdOfNewResource(); addLinkHeaderOnResourceCreation(response, idOfNewResource); } void addLinkHeaderOnResourceCreation (HttpServletResponse response, long idOfNewResource){ URI uri = ServletUriComponentsBuilder.fromCurrentRequestUri(). path("/{idOfNewResource}").buildAndExpand(idOfNewResource).toUri(); response.setHeader("Location", uri.toASCIIString()); } }

In questo esempio, stiamo usando ServletUriComponentsBuilder , che aiuta con l'utilizzo della richiesta corrente. In questo modo, non abbiamo bisogno di passare nulla e possiamo semplicemente accedervi staticamente.

Se l'API restituisse ResponseEntity , potremmo anche utilizzare il supporto per la posizione .

4. Ottenere una singola risorsa

Al recupero di una singola risorsa, il client dovrebbe essere in grado di scoprire l'URI per ottenere tutte le risorse di quel tipo:

@Component class SingleResourceRetrievedDiscoverabilityListener implements ApplicationListener{ @Override public void onApplicationEvent(SingleResourceRetrieved resourceRetrievedEvent){ Preconditions.checkNotNull(resourceRetrievedEvent); HttpServletResponse response = resourceRetrievedEvent.getResponse(); addLinkHeaderOnSingleResourceRetrieval(request, response); } void addLinkHeaderOnSingleResourceRetrieval(HttpServletResponse response){ String requestURL = ServletUriComponentsBuilder.fromCurrentRequestUri(). build().toUri().toASCIIString(); int positionOfLastSlash = requestURL.lastIndexOf("/"); String uriForResourceCreation = requestURL.substring(0, positionOfLastSlash); String linkHeaderValue = LinkUtil .createLinkHeader(uriForResourceCreation, "collection"); response.addHeader(LINK_HEADER, linkHeaderValue); } }

Si noti che la semantica della relazione di collegamento fa uso del tipo di relazione "raccolta" , specificato e utilizzato in diversi microformati, ma non ancora standardizzato.

L' intestazione Link è una delle intestazioni HTTP più utilizzate ai fini della rilevabilità. L'utilità per creare questa intestazione è abbastanza semplice:

public class LinkUtil { public static String createLinkHeader(String uri, String rel) { return "; rel=\"" + rel + "\""; } }

5. Rilevabilità alla radice

La radice è il punto di ingresso dell'intero servizio: è ciò con cui il client entra in contatto quando utilizza l'API per la prima volta.

Se il vincolo HATEOAS deve essere considerato e implementato in tutto, allora questo è il punto di partenza. Pertanto tutti gli URI principali del sistema devono essere rilevabili dalla radice.

Diamo ora un'occhiata al controller per questo:

@GetMapping("/") @ResponseStatus(value = HttpStatus.NO_CONTENT) public void adminRoot(final HttpServletRequest request, final HttpServletResponse response) { String rootUri = request.getRequestURL().toString(); URI fooUri = new UriTemplate("{rootUri}{resource}").expand(rootUri, "foos"); String linkToFoos = LinkUtil.createLinkHeader(fooUri.toASCIIString(), "collection"); response.addHeader("Link", linkToFoos); }

Questa è, ovviamente, un'illustrazione del concetto, concentrandosi su un singolo URI di esempio, per Foo Resources. Una reale implementazione dovrebbe aggiungere, in modo simile, gli URI per tutte le risorse pubblicate nel client.

5.1. La rilevabilità non riguarda la modifica degli URI

Questo può essere un punto controverso: da un lato, lo scopo di HATEOAS è fare in modo che il client scopra gli URI dell'API e non si basi su valori hardcoded. D'altra parte, non è così che funziona il Web: sì, gli URI vengono scoperti, ma sono anche segnalibri.

Una distinzione sottile ma importante è l'evoluzione dell'API: i vecchi URI dovrebbero ancora funzionare, ma qualsiasi client che scoprirà l'API dovrebbe scoprire i nuovi URI, il che consente all'API di cambiare dinamicamente e ai buoni client di funzionare bene anche quando Modifiche API.

In conclusione, solo perché tutti gli URI del servizio Web RESTful dovrebbero essere considerati URI interessanti (e gli URI interessanti non cambiano), ciò non significa che aderire al vincolo HATEOAS non sia estremamente utile quando si evolve l'API.

6. Avvertenze sulla rilevabilità

Come affermano alcune delle discussioni sugli articoli precedenti, il primo obiettivo della rilevabilità è fare un uso minimo o nullo della documentazione e fare in modo che il client impari e comprenda come utilizzare l'API tramite le risposte che ottiene.

In effetti, questo non dovrebbe essere considerato un ideale così inverosimile - è il modo in cui utilizziamo ogni nuova pagina web - senza alcuna documentazione. Quindi, se il concetto è più problematico nel contesto di REST, allora deve essere una questione di implementazione tecnica, non di una questione se sia possibile o meno.

Detto questo, tecnicamente, siamo ancora lontani da una soluzione completamente funzionante: le specifiche e il supporto del framework sono ancora in evoluzione e, per questo motivo, dobbiamo scendere a compromessi.

7. Conclusione

Questo articolo ha trattato l'implementazione di alcuni tratti della rilevabilità nel contesto di un servizio RESTful con Spring MVC e ha toccato il concetto di rilevabilità alla radice.

L'implementazione di tutti questi esempi e frammenti di codice può essere trovata su GitHub: questo è un progetto basato su Maven, quindi dovrebbe essere facile da importare ed eseguire così com'è.

REST fondo

Ho appena annunciato il nuovo corso Learn Spring , incentrato sui fondamenti di Spring 5 e Spring Boot 2:

>> SCOPRI IL CORSO