Рубрики
Без рубрики

HATEOAS для весеннего ОТДЫХА

Реализация HATEOAS и возможность обнаружения для API Spring REST.

Автор оригинала: Eugen Paraschiv.

1. Обзор

Эта статья будет посвящена реализации возможности обнаружения в службе весеннего отдыха и удовлетворению ограничения 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;
    }
}

Затем, контроллер, с 2 простыми операциями – найти по идентификатору и создать :

@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 этого ресурса в Location HTTP-заголовке ответа.

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

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

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

Если API вернет ResponseEntity – мы также могли бы использовать Location support .

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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