HATEOAS para um serviço REST de primavera
1. Visão geral
Este artigo se concentrará emimplementation of discoverability in a Spring REST Servicee em satisfazer a restrição HATEOAS.
2. Dissociando a descoberta por meio de eventos
Discoverability as a separate aspect or concern of the web layer should be decoupled from the controller tratando da solicitação HTTP. Para esse propósito, o Controlador dispara eventos para todas as ações que requerem manipulação adicional da resposta.
Primeiro, vamos criar os eventos:
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;
}
}
Então,the Controller, with 2 simple operations – find by id and create:
@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));
}
}
We can then handle these events with any number of decoupled listeners. Cada um deles pode se concentrar em seu próprio caso particular e ajudar a satisfazer a restrição geral do HATEOAS.
Os ouvintes devem ser os últimos objetos na pilha de chamadas e não é necessário acesso direto a eles; como tal, eles não são públicos.
3. Tornar o URI de um recurso recém-criado detectável
Conforme discutido emprevious post on HATEOAS,the operation of creating a new Resource should return the URI of that resource in the Location HTTP header da resposta.
Faremos isso usando um ouvinte:
@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());
}
}
Neste exemplo,we’re making use of the ServletUriComponentsBuilder - o que ajuda a usar a solicitação atual. Dessa forma, não precisamos passar nada e podemos simplesmente acessar isso estaticamente.
Se a API retornasseResponseEntity - também poderíamos usarLocation support.
4. Obtendo um único recurso
Ao recuperar um único Recurso,the client should be able to discover the URI to get all Resources desse 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);
}
}
Observe que a semântica da relação de link faz uso do tipo de relação“collection”, especificado e usado emseveral microformats, mas ainda não padronizado.
The Link header is one of the most used HTTP headersfor the purposes of discoverability. O utilitário para criar este cabeçalho é bastante simples:
public class LinkUtil {
public static String createLinkHeader(String uri, String rel) {
return "<" + uri + ">; rel="" + rel + """;
}
}
5. Detecção na raiz
A raiz é o ponto de entrada em todo o serviço - é o que o cliente entra em contato ao consumir a API pela primeira vez.
Se a restrição HATEOAS deve ser considerada e implementada por toda parte, então este é o ponto de partida. Portanto,all the main URIs of the system have to be discoverable from the root.
Vejamos agora o controlador para isso:
@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);
}
Esta é, obviamente, uma ilustração do conceito, focando em um único URI de amostra, paraFoo Recursos. Uma implementação real deve adicionar, da mesma forma, URIs para todos os recursos publicados no cliente.
5.1. A descoberta não é sobre a mudança de URIs
Esse pode ser um ponto controverso - por um lado, o objetivo do HATEOAS é fazer com que o cliente descubra os URIs da API e não confie em valores codificados. Por outro lado - não é assim que a web funciona: sim, os URIs são descobertos, mas também são marcados.
Uma distinção sutil, mas importante, é a evolução da API - os URIs antigos ainda devem funcionar, mas qualquer cliente que descubra a API deve descobrir os novos URIs - o que permite que a API mude dinamicamente e bons clientes funcionem bem, mesmo quando o Alterações de API.
Concluindo - só porque todos os URIs do serviço da web RESTful devem ser consideradoschttps: //www.w3.org/TR/cooluris/ [ool]URIs (e URIs legaisdon’t change ) - isso não significa que aderir à restrição HATEOAS não seja extremamente útil ao desenvolver a API.
6. Advertências de descoberta
Como algumas das discussões em torno dos artigos anteriores afirmam,the first goal of discoverability is to make minimal or no use of documentation e faça o cliente aprender e entender como usar a API por meio das respostas que obtém.
Na verdade, isso não deve ser considerado um ideal tão rebuscado - é como consumimos cada nova página da web - sem qualquer documentação. Portanto, se o conceito é mais problemático no contexto do REST, deve ser uma questão de implementação técnica, não uma questão de se é ou não possível.
Dito isto, tecnicamente, ainda estamos longe de uma solução totalmente funcional - a especificação e o suporte à estrutura ainda estão evoluindo e, por isso, temos que fazer alguns compromissos.
7. Conclusão
Este artigo abordou a implementação de algumas das características de descoberta no contexto de um serviço RESTful com Spring MVC e abordou o conceito de descoberta na raiz.
A implementação de todos esses exemplos e trechos de código pode ser encontrada emmy GitHub project - este é um projeto baseado em Maven, portanto, deve ser fácil de importar e executar como está.