Автор оригинала: 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, поэтому его должно быть легко импортировать и запускать как есть.