HATEOAS для службы Spring REST
1. обзор
В этой статье основное внимание будет уделеноimplementation of discoverability in a Spring REST Service и соблюдению ограничения HATEOAS.
2. Разделение возможности обнаружения через события
Discoverability as a separate aspect or concern of the web layer should be decoupled from the controller обрабатывает HTTP-запрос. Для этого Контроллер будет запускать события для всех действий, которые требуют дополнительных манипуляций с ответом.
Во-первых, давайте создадим события:
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;
}
}
Тогда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. Каждый из них может сосредоточиться на своем конкретном случае и помочь в удовлетворении общего ограничения HATEOAS.
Слушатели должны быть последними объектами в стеке вызовов, и прямой доступ к ним не требуется; как таковые они не являются публичными.
3. Обеспечение доступности URI для вновь созданного ресурса для обнаружения
Как обсуждалось в ответахprevious post on HATEOAS,the operation of creating a new Resource should return the URI of that resource in the Location HTTP header.
Мы справимся с этим с помощью слушателя:
@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());
}
}
В этом примереwe’re making use of the ServletUriComponentsBuilder - помогает использовать текущий запрос. Таким образом, нам не нужно ничего передавать, и мы можем просто получить доступ к этому статически.
Если API вернетResponseEntity - мы также могли бы использоватьLocation support.
4. Получение единого ресурса
При получении одного ресурсаthe client should be able to discover the URI to get all Resources этого типа:
@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);
}
}
Обратите внимание, что семантика отношения ссылок использует тип отношения“collection”, указанный и используемый вseveral microformats, но еще не стандартизованный.
The Link header is one of the most used HTTP headersfor the purposes of discoverability. Утилита для создания этого заголовка достаточно проста:
public class LinkUtil {
public static String createLinkHeader(String uri, String rel) {
return "<" + uri + ">; rel="" + rel + """;
}
}
5. Открываемость в корне
Корень - это точка входа для всей службы - это то, с чем контактирует клиент при первом использовании API.
Если ограничение HATEOAS должно быть рассмотрено и реализовано повсеместно, то это место для начала. Следовательно,all the main URIs of the system have to be discoverable from the root.
Давайте теперь посмотрим на контроллер:
@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);
}
Это, конечно, иллюстрация концепции, фокусирующаяся на одном образце URI для ресурсовFoo. Реальная реализация должна добавить аналогичным образом URI для всех ресурсов, опубликованных для клиента.
5.1. Обнаруживаемость - это не изменение URI
Это может быть спорным моментом - с одной стороны, цель HATEOAS состоит в том, чтобы клиент обнаруживал URI API, а не полагался на жестко закодированные значения. С другой стороны - это не то, как работает сеть: да, URI обнаруживаются, но они также добавляются в закладки.
Тонкое, но важное различие заключается в эволюции API - старые URI все еще должны работать, но любой клиент, который обнаружит API, должен обнаруживать новые URI - что позволяет динамически менять API, а хорошие клиенты работают хорошо, даже когда Изменения API.
В заключение - просто потому, что все URI веб-службы RESTful следует рассматриватьchttps: //www.w3.org/TR/cooluris/ [ool]URIs (и классные URIdon’t change ) - это не означает, что соблюдение ограничения HATEOAS не очень полезно при развитии API.
6. Предостережения об обнаружимости
Как отмечается в некоторых обсуждениях предыдущих статей,the first goal of discoverability is to make minimal or no use of documentation и клиент учат и понимают, как использовать API через полученные ответы.
На самом деле, это не следует рассматривать как такой надуманный идеал - именно так мы потребляем каждую новую веб-страницу - без какой-либо документации. Таким образом, если концепция является более проблематичной в контексте REST, то это должен быть вопрос технической реализации, а не вопрос о том, возможно ли это.
При этом технически мы все еще далеки от полностью работающего решения - поддержка спецификаций и фреймворка все еще развивается, и из-за этого нам приходится идти на некоторые компромиссы.
7. Заключение
В этой статье рассматривается реализация некоторых признаков обнаруживаемости в контексте службы RESTful с Spring MVC и затрагивается концепция обнаружения в корне.
Реализация всех этих примеров и фрагментов кода можно найти вmy GitHub project - это проект на основе Maven, поэтому его должно быть легко импортировать и запускать как есть.