Вступление
REST API гибки и позволяют разработчикам создавать независимые системы. С появлением микросервисной архитектуры REST стал еще более зрелым, поскольку микросервисы можно создавать независимо от языка или структуры, используемой в приложении.
Быть «в центре внимания» - это означает, что новые типы производятся или создаются на основе REST API, что приводит нас к HATEOAS.
Что такое HATEOAS?
В центре внимания представлены различные архитектурные техники, ориентированные на основы REST.
Гипермедиа как механизм состояния приложения (HATEOAS) - это архитектурный подход для повышения удобства использования REST API для приложений, использующих API.
Основная цель HATEOAS - предоставить дополнительную информацию в ответах REST API, чтобы пользователи API могли получить дополнительные сведения о конечной точке за один вызов. Это позволяет пользователям создавать свои системы с динамическими вызовами API, перемещаясь от одной конечной точки к другой, используя информацию, полученную при каждом вызове.
Чтобы лучше понять это, взгляните на следующий ответ API:
{
"id": 1,
"name": "Dr. Sanders",
"speciality": "General",
"patientList": [
{
"id": 1,
"name": "J. Smalling",
"_links": {
"self": {
"href": "http://localhost:8080/patients/1"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
},
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
}
Помимо получения подробной информации о докторе, ответ API также предоставляет дополнительную информацию в виде ссылок. Например, прикреплена ссылка для получения всех пациентов одного врача.
У нас есть ответ, обогащенный ресурсами , где предоставленные ссылки - это ресурсы, которые обогащают наш ответ дополнительной информацией.
Весна HATEOAS
Spring HATEOAS предоставляет библиотеки для простой реализации архитектуры HATEOAS в приложении Spring. Используя Spring HATEOAS API, ссылки можно создавать и возвращать как часть объекта ответа API.
Зависимости Spring HATEOAS
Используя Maven, добавить Spring HATEOAS так же просто, как включить зависимости:
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>[2.0.0.RELEASE,)</version>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>[1.0.3.RELEASE,)</version>
</dependency>
В качестве альтернативы, используя Gradle, вы можете добавить:
implementation 'org.springframework.plugin:spring-plugin-core:2.+'
implementation 'org.springframework.hateoas:spring-hateoas:1.+'
Зависимости Spring Boot HATEOAS
Еще проще, для приложений Spring Boot вы можете использовать
spring-boot-starter-hateoas
Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<version>[2.2.4.RELEASE,)</version>
</dependency>
Точно так же, если вы используете Gradle, вы можете просто добавить:
implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.+'
Использование spring-boot-starter-hateoas
включает в себя зависимости
spring-hateoas
и spring-boot-starter-web
, поэтому, естественно,
никаких других стартеров не требуется.
Пружинные строительные блоки HATEOAS
Основные строительные блоки для Spring HATEOAS являются Link
s и
RepresentationModel
s (контейнер для сбора Link
s).
Затем RepresentationModel
расширяется до EntityModel
(для отдельных
ресурсов) и CollectionModel
(для нескольких ресурсов), а также до
PagedModel
.
Давайте кратко объясним каждый из них, прежде чем реализовывать их в рабочей демонстрации.
Ссылки
Неизменяемый Link
используется для хранения метаданных для ресурса
(URI или местоположения), а конечный пользователь может переходить к
ресурсам, которые дополняют наш ответ API. Базовая ссылка с URI ресурса
может выглядеть так:
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
}
}
Ссылка содержит href
, указывающий на URI ресурса. href
заключен в
self
тег, который определяет связь с сущностью. Это означает, что
ресурс, по сути, указывает на себя.
Почему ресурс указывает на себя?
Возвращенные ресурсы могут не быть полным представлением самих себя. У врача может быть список пациентов, но мы, возможно, не захотим возвращать его по умолчанию.
Если затем мы захотим взглянуть на список врачей, мы можем перейти к нему по ссылке.
Модели представления
RepresentationModel
выступает в качестве корневого класса для всех
других классов моделей Spring HATEOAS. Он содержит коллекцию Link
и
предоставляет метод для их добавления / удаления.
Создание собственной модели так же легко , как расширение
RepresentationModel
класса. В противном случае вы можете использовать
любую из готовых моделей:
-
Модель сущности :
EntityModel
используется для представления ресурса, который соответствует одному объекту. Вы можете обернуть свой ресурсEntityModel
и передать его вызывающей службе или вернуть его через конечную точку REST. -
Модель коллекции : Подобно
EntityModel
,CollectionModel
используется для обертывания ресурсов, хотя она обертывает ресурс, который соответствует коллекции объектов. -
Модель с
PagedModel
Кроме того, поскольку многие конечные точки REST API возвращают ответы, которые представляют собой страничные коллекции, Spring HATEOAS предоставляет модель PagedModel для представления таких ресурсов.
Создание ссылок
Давайте создадим ресурс образца , который extends
в
RepresentationModel
класс:
public class Doctor extends RepresentationModel<Doctor> {
private int id;
private List<Patient> patientList;
}
На данный момент наша Doctor
имеет только id
и список пациентов.
Затем мы добавим Link
на ресурс, которая будет указывать ресурс на
самого себя.
Связать объект
Объекты Spring HATEOAS Link
принимают String
для указания URI и
отношения между объектами. В основном это атрибуты href
и rel
Link selfLink = new Link("http://localhost:8080/doctors/1", "self");
Doctor doctor = new Doctor();
doctor.add(selfLink);
Когда объект Doctor возвращается (как показано в демонстрационном приложении в следующих разделах), тело ответа будет содержать:
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
}
}
MVC LinkBuilder
Однако жесткое кодирование значений в конструкторе Link
не
рекомендуется. По мере роста вашего приложения / API быстро становится
трудно управлять ими и обновлять их. Чтобы бороться с этим, мы можем
использовать WebMvcLinkBuilder
, который позволяет нам создавать
ссылки, используя классы контроллеров и указывая на их методы.
Давайте воссоздадим ссылку из предыдущего примера с помощью
WebMvcLinkBuilder
:
Link link = linkTo(methodOn(DoctorController.class).getDoctorById(id)).withSelfRel();
Здесь мы используем более программный подход к созданию ссылок. Он
указывает на метод getDoctorById()
внутри класса DoctorController
Поскольку он указывает на себя, мы используем метод withSelfRel()
для
определения отношения.
В качестве альтернативы мы могли бы использовать метод withRel()
и
передать String с другим отношением.
Spring HATEOAS переведет детали конечной точки из класса контроллера и
метода, который мы предоставили, в WebMvcLinkBuilder
. Выходные данные
этого Link
будут точно такими же, как и в предыдущем примере.
Реляционные ссылки
Чтобы создать ссылки на ресурсы, которые связаны между собой или
указывают на другой ресурс, мы будем использовать метод withRel()
.
Используя это, мы можем указать конечную точку, с которой можно получить
доступ к связанному ресурсу:
Link link = linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId()))
.withRel("patientList");
Приведенный выше фрагмент кода указывает, что пользователь может
получить список patientList
для doctor
, используя метод
getDoctorPatients()
внутри класса DoctorController
При добавлении в
тело ответа он генерирует следующую ссылку:
"_links": {
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
Обратите внимание, что мы не указали URL-адрес при создании ссылки. Spring HATEOAS может извлекать информацию из построителя ссылок и генерировать URL-адрес на основе использованных нами сопоставлений.
Конфигурация
Чтобы правильно отображать различные RepresentationModel
, можно
включить представление @EnableHypermediaSupport
аннотации @
EnableHypermediaSupport. Вы можете передать HypermediaType
в качестве
аргумента этой аннотации, что позволит вам указать тип гипермедиа,
например JSON,
UBER
, HAL и
т. Д. Использование аннотации позволяет Spring настраивать необходимые
модули Джексона для правильного рендеринга гипермедиа.
Обычно Spring обнаруживает используемый вами стек технологий и автоматически настраивает конфигурацию при добавлении аннотации. Однако, если у вас есть какие-то особые требования, мы рекомендуем вам ознакомиться с официальной документацией .
Демо-приложение
С учетом всего сказанного, давайте напишем простое приложение Spring с
поддержкой HATEOAS, перейдя к Spring
Initializr и сгенерировав пустое приложение
Spring Boot с Spring HATEOAS
( spring-boot-hateoas-starter
):
{.ezlazyload}
Создание ресурса
Для любого ресурса, который должен быть предоставлен через REST API, он
должен расширять RepresentationModel
. Расширяя RepresentationModel
класса, мы также наследуем add()
метод, который используется для
крепления ссылки на него.
Давайте создадим модель Doctor
:
public class Doctor extends RepresentationModel<Doctor> {
private int id;
private String name;
private String speciality;
private List<Patient> patientList;
}
Поскольку Doctor
имеет отношение к пациентам, давайте также создадим
модель Patient
public class Patient extends RepresentationModel<Patient> {
private int id;
private String name;
}
Затем в контроллере, в нашем случае DoctorController
, мы
автоматически DoctorService
:
@RestController
@RequestMapping(value = "/doctors")
public class DoctorController {
@Autowired
DoctorService doctorService;
}
Как и следовало ожидать, он содержит такие методы, как getDoctor()
,
getDoctorWithPatients()
, getDoctors()
и т. Д., Doctor
или
List<Doctor>
. Реализация опущена для краткости - если вы хотите
взглянуть, код размещен на
GitHub .
Таким образом, мы создали ресурс. При извлечении ресурсов мы ожидаем
либо один ресурс, либо набор ресурсов. Как было сказано ранее, мы
EntityModel
их в EntityModel или CollectionModel
соответственно.
Получение одного ресурса
Давайте сначала реализуем функцию получения одного врача. Поскольку мы
ожидаем, что вызов API вернет один ресурс, мы EntityModel
наш ответ в
класс EntityModel:
@GetMapping(value = "/{id}")
public EntityModel<Doctor> getDoctorById(@PathVariable int id) {
Doctor doctor = doctorService.getDoctorWithPatients(id);
for (final Patient patient : doctor.getPatientList()) {
Link selfLink = linkTo(methodOn(PatientController.class)
.getPatientById(patient.getId())).withSelfRel();
patient.add(selfLink);
}
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorById(id)).withSelfRel());
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId())).withRel("patientList"));
return new EntityModel<>(doctor);
}
После получения Doctor
мы просматриваем список связанных пациентов и
добавляем ссылку для каждого из них. Каждую из этих ссылок можно
использовать для PatientController
Patient
через PatientController.
Точно так же мы добавляем self
ссылку на Doctor
которая
использовалась для вызовов API. Наряду с собственной ссылкой мы также
добавляем реляционную ссылку, указывающую на список пациентов.
В конце метода мы обернули наш объект Doctor
EntityModel
и эта
EntityModel
возвращается в качестве ответа:
{
"id": 1,
"name": "Dr. Sanders",
"speciality": "General",
"patientList": [
{
"id": 1,
"name": "J. Smalling",
"_links": {
"self": {
"href": "http://localhost:8080/patients/1"
}
}
},
{
"id": 2,
"name": "Samantha Williams",
"_links": {
"self": {
"href": "http://localhost:8080/patients/2"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
},
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
}
«Доктор Сандерс» имеет «Дж. Смоллинг» и «Саманту Уильямс» в качестве своих пациентов, и конечная точка для врача и конечная точка для списка пациентов доктора добавляются к ответу, что делает его более ресурсоемким .
Получение нескольких ресурсов
Давайте создадим еще один вызов GET, который вернет всех доступных
врачей в системе. Теперь, когда ожидаемый ответ будет набором Doctor
,
мы заключим ответ в CollectionModel
:
@GetMapping
public CollectionModel<Doctor> getDoctors() {
List<Doctor> doctors = doctorService.getDoctorsWithPatients();
for (final Doctor doctor : doctors) {
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorById(doctor.getId())).withSelfRel());
doctor.add(linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId())).withRel("patientList"));
for (final Patient patient : doctor.getPatientList()) {
Link selfLink = linkTo(methodOn(PatientController.class)
.getPatientById(patient.getId())).withSelfRel();
patient.add(selfLink);
}
}
Link link = linkTo(methodOn(DoctorController.class).getDoctors()).withSelfRel();
return new CollectionModel<>(doctors, link);
}
В этом методе, наряду с self
ссылкой для самого вызова REST, мы также
добавляем ссылку на себя для получения каждого отдельного врача. У
каждого врача есть родственная связь, которая указывает на
ассоциированных пациентов. Внутри списка пациентов у каждого пациента
также есть self
, по которой также можно найти конкретного пациента.
После добавления всех ссылок мы завернули коллекцию объектов Doctor
CollectionModel
и вернули ее:
{
"_embedded": {
"doctorList": [
{
"id": 1,
"name": "Dr. Sanders",
"speciality": "General",
"patientList": [
{
"id": 1,
"name": "J. Smalling",
"_links": {
"self": {
"href": "http://localhost:8080/patients/1"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
},
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
},
{
"id": 2,
"name": "Dr. Goldberg",
"speciality": "General",
"patientList": [
{
"id": 4,
"name": "K. Oliver",
"_links": {
"self": {
"href": "http://localhost:8080/patients/4"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/doctors/2"
},
"patientList": {
"href": "http://localhost:8080/doctors/2/patients"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/doctors"
}
}
}
Как видно из выходных данных, просто сделав один вызов, пользователь может обнаружить дополнительную информацию, которая иначе не была бы представлена.
Заключение
Spring HATEOAS предоставляет необходимые библиотеки и инфраструктуру для реализации архитектуры HATEOAS в приложениях на основе Spring.
Как видно из выходных данных, пользователи могут получить дополнительную информацию из одного вызова REST. Используя эту информацию, проще создавать динамические клиенты REST.
В этой статье мы обсудили, как работает HATEOAS, его реализацию в Spring, и закончили построением простого приложения для демонстрации концепций.
Исходный код примера кода можно найти здесь, на GitHub .