Перейти к основному содержимому

HATEOAS для службы Spring REST

· 5 мин. чтения

1. Обзор

В этой статье основное внимание будет уделено реализации возможности обнаружения в службе Spring REST и удовлетворению ограничения HATEOAS.

Эта статья посвящена Spring MVC. В нашей статье Введение в Spring HATEOAS описывается, как использовать HATEOAS в Spring Boot.

2. Разделение возможности обнаружения с помощью событий

Обнаруживаемость как отдельный аспект или проблема веб-уровня должна быть отделена от контроллера , обрабатывающего 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;
}
}

Затем Контроллер с двумя простыми операциями — найти по id и создать :

@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));
}
}

Затем мы можем обрабатывать эти события с любым количеством несвязанных слушателей. Каждый из них может сосредоточиться на своем конкретном случае и помочь удовлетворить общее ограничение HATEOAS.

Слушатели должны быть последними объектами в стеке вызовов, и прямой доступ к ним не требуется; как таковые они не являются публичными.

3. Сделать URI вновь созданного ресурса доступным для обнаружения

Как обсуждалось в предыдущем сообщении о HATEOAS , операция создания нового ресурса должна возвращать URI этого ресурса в HTTP-заголовке Location ответа.

Мы справимся с этим с помощью слушателя:

@Component
class ResourceCreatedDiscoverabilityListener
implements ApplicationListener<ResourceCreated>{

@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());
}
}

В этом примере мы используем ServletUriComponentsBuilder , который помогает использовать текущий запрос. Таким образом, нам не нужно ничего передавать, и мы можем просто получить к этому статический доступ.

Если бы API возвращал ResponseEntity — мы могли бы также использовать поддержку Location .

4. Получение единого ресурса

При получении одного ресурса клиент должен иметь возможность обнаружить URI для получения всех ресурсов этого типа:

@Component
class SingleResourceRetrievedDiscoverabilityListener
implements ApplicationListener<SingleResourceRetrieved>{

@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);
}
}

Обратите внимание, что семантика отношения ссылки использует тип отношения «коллекция» , указанный и используемый в нескольких микроформатах , но еще не стандартизированный.

Заголовок Link является одним из наиболее часто используемых заголовков HTTP для обеспечения возможности обнаружения. Утилита для создания этого заголовка достаточно проста:

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

5. Возможность обнаружения в корне

Корень — это точка входа во всю службу — это то, с чем клиент вступает в контакт при первом использовании API.

Если ограничение HATEOAS необходимо учитывать и внедрять повсюду, то это место для начала. Поэтому все основные URI системы должны быть доступны для обнаружения из корня.

Давайте теперь посмотрим на контроллер для этого:

@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 Resources. Реальная реализация должна аналогичным образом добавлять URI для всех ресурсов, опубликованных для клиента.

5.1. Обнаруживаемость не связана с изменением URI

Это может быть спорным моментом — с одной стороны, цель HATEOAS состоит в том, чтобы клиент обнаружил URI API, а не полагался на жестко закодированные значения. С другой стороны, веб так не работает: да, URI обнаруживаются, но они также добавляются в закладки.

Тонким, но важным отличием является эволюция API: старые URI должны по-прежнему работать, но любой клиент, который обнаружит API, должен обнаружить новые URI, что позволяет API динамически изменяться, а хорошие клиенты будут работать хорошо, даже когда Изменения API.

В заключение — только потому, что все URI веб-службы RESTful следует считать классными URI (а классные URI не меняются ) — это не означает, что соблюдение ограничения HATEOAS не очень полезно при развитии API .

6. Предостережения относительно возможности обнаружения

Как говорится в некоторых обсуждениях предыдущих статей, первая цель возможности обнаружения состоит в том, чтобы свести документацию к минимуму или вообще не использовать ее , а также заставить клиента учиться и понимать, как использовать API с помощью получаемых им ответов.

На самом деле, это не следует рассматривать как надуманный идеал — именно так мы потребляем каждую новую веб-страницу — без какой-либо документации. Итак, если концепция более проблематична в контексте REST, то это должно быть вопросом технической реализации, а не вопроса о том, возможно ли это.

При этом технически мы все еще далеки от полностью рабочего решения — спецификация и поддержка фреймворка все еще развиваются, и из-за этого нам приходится идти на некоторые компромиссы.

7. Заключение

В этой статье рассматривается реализация некоторых признаков обнаруживаемости в контексте службы RESTful с помощью Spring MVC и затрагивается концепция обнаруживаемости в корне.

Реализацию всех этих примеров и фрагментов кода можно найти на GitHub — это проект на основе Maven, поэтому его легко импортировать и запускать как есть.