Spring HATEOAS: веб-службы RESTful, управляемые гипермедиа

Введение REST API гибки и позволяют разработчикам создавать независимые системы. С появлением микросервисной архитектуры REST стал еще более зрелым, поскольку микросервисы можно создавать независимо от языка или структуры, используемой в приложении. Быть «в центре внимания» - это означает, что новые типы производятся или создаются на основе REST API, что подводит нас к HATEOAS. Что такое HATEOAS? В центре внимания находятся различные архитектурные техники, ориентированные на основы REST.

Вступление

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 ):

пустой проект springinitializr{.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 .

comments powered by Disqus