Introducción
Contenido
Las API REST son flexibles y permiten a los desarrolladores crear sistemas desacoplados. Con el auge de la arquitectura de microservicios, REST ha madurado aún más, ya que los microservicios se pueden construir independientemente del lenguaje o el marco utilizado en la aplicación.
Estar “en el centro de atención”: esto significa que los nuevos tipos se derivan o se construyen alrededor de las API REST, lo que nos lleva a HATEOAS.
¿Qué es HATEOAS?
Al estar en el centro de atención, se están introduciendo diferentes técnicas de arquitectura centradas en los fundamentos de REST.
Hypermedia as the Engine of Application State (HATEOAS) es un enfoque arquitectónico para mejorar la usabilidad de las API REST para las aplicaciones que consumen las API.
El propósito principal de HATEOAS es proporcionar información adicional en las respuestas de la API REST para que los usuarios de la API puedan obtener detalles adicionales del punto final de una sola llamada. Esto permite a los usuarios construir sus sistemas con llamadas API dinámicas, pasando de un punto final a otro utilizando la información recuperada de cada llamada.
Para comprender esto mejor, eche un vistazo a la siguiente respuesta de la 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"
}
}
}
Además de obtener los detalles sobre el médico, la respuesta de la API también proporciona información adicional en forma de enlaces. Por ejemplo, también se adjunta un enlace para buscar a todos los pacientes de un solo médico.
Lo que tenemos aquí es una respuesta enriquecida en recursos, donde los enlaces proporcionados son recursos que enriquecen nuestra respuesta con información adicional.
Spring HATEOAS
Spring HATEOAS proporciona bibliotecas para implementar la arquitectura HATEOAS en una aplicación Spring con facilidad. Usando la API de Spring HATEOAS, los enlaces se pueden crear y devolver como parte del objeto de respuesta de la API.
Dependencias de Spring HATEOAS
Usando Maven, agregar Spring HATEOAS es tan fácil como incluir las dependencias:
org.springframework.plugin spring-plugin-core [2.0.0.RELEASE,)</version>
</dependency>
<dependency>
<groupId>org.springframework.hateoas</groupId>
<artifactId>spring-hateoas</artifactId>
<version>[1.0.3.RELEASE,)</version>
</dependency>
Alternativamente, usando Gradle, puede agregar:
implementation 'org.springframework.plugin:spring-plugin-core:2.+'
implementation 'org.springframework.hateoas:spring-hateoas:1.+'
Spring Boot HATEOAS dependencias
Aún más fácil, para las aplicaciones Spring Boot, puede usar la spring-boot-starter-hateoas
dependencia de Maven:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
<version>[2.2.4.RELEASE,)</version>
</dependency>
Del mismo modo, si está usando Gradle, simplemente puede agregar:
implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.+'
El uso de spring-boot-starter-hateoas
dependencias incluye spring-hateoas
y spring-boot-starter-web
dependencias, por lo que, naturalmente, no se necesitan otros iniciadores.
Bloques de construcción Spring HATEOAS
Los bloques de construcción básicos para Spring HATEOAS son Link
s y RepresentationModel
s (un contenedor para una colección de Link
s).
El RepresentationModel
se extiende luego en EntityModel
(para recursos individuales) y CollectionModel
(para múltiples recursos), así como una PagedModel
.
Tomemos un breve momento para explicar cada uno de estos antes de implementarlos en una demostración de trabajo.
Enlaces
El Link
objeto inmutable se usa para almacenar metadatos para un recurso (URI o ubicación), y el usuario final puede navegar a los recursos que enriquecen nuestra respuesta API. Un enlace básico con un URI de recurso podría verse así:
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
}
}
El enlace contiene un href
atributo que apunta al URI del recurso. El href
atributo está envuelto dentro de una self
etiqueta, que identifica la relación con la entidad. Esto significa que el recurso se apunta a sí mismo, esencialmente.
¿Por qué el recurso apunta a sí mismo?
Es posible que los recursos devueltos no sean la representación completa de sí mismos. Un médico puede tener una lista de pacientes, pero es posible que no queramos devolverla de forma predeterminada.
Si luego quisiéramos echar un vistazo a la lista de médicos, podemos navegar a través del enlace.
Modelos de representación
El RepresentationModel
actúa como una clase raíz de todas las demás clases del modelo de Spring HATEOAS. Contiene una colección de Link
mensajes de correo electrónico y proporciona un método para agregarlos / eliminarlos.
Crear tu propio modelo es tan fácil como ampliar la RepresentationModel
clase. De lo contrario, puede utilizar cualquiera de los modelos disponibles:
- Modelo de entidad :
EntityModel
se utiliza para representar un recurso que corresponde a un solo objeto. Puede envolver su recurso conEntityModel
y pasarlo a un servicio de llamada o devolverlo a través de un punto final REST. - Modelo de colección : similar a
EntityModel
,CollectionModel
se usa para envolver recursos; sin embargo, envuelve un recurso que corresponde a una colección de objetos. - Modelo paginado : Además, dado que muchos puntos finales de la API REST devuelven respuestas, que son colecciones paginables, Spring HATEOAS proporciona la forma
PagedModel
de representar dichos recursos.
Crear enlaces
Creemos un recurso de muestra que extends
la RepresentationModel
clase:
public class Doctor extends RepresentationModel<Doctor> {
private int id;
private List<Patient> patientList;
}
Por ahora nuestro Doctor
modelo solo tiene una id
propiedad y una lista de pacientes. A continuación, agregaremos un Link
al recurso, que apuntará el recurso a sí mismo.
Objeto de enlace
Los Link
objetos Spring HATEOAS toman String
argumentos para especificar el URI y la relación entre las entidades. Estos son básicamente los atributos href
y rel
:
Link selfLink = new Link("http://localhost:8080/doctors/1", "self");
Doctor doctor = new Doctor();
doctor.add(selfLink);
Cuando se devuelve un objeto doctor (como se muestra en la aplicación de demostración en secciones posteriores), el cuerpo de respuesta contendrá:
"_links": {
"self": {
"href": "http://localhost:8080/doctors/1"
}
}
MVC LinkBuilder
Sin Link
embargo, no se recomienda codificar valores en el constructor de la clase. Rápidamente se vuelve difícil administrarlos y actualizarlos a medida que crece su aplicación / API. Para combatir esto, podemos usar el WebMvcLinkBuilder
, que nos permite crear enlaces usando clases de controlador y apuntando a sus métodos.
Recreemos el enlace del ejemplo anterior usando WebMvcLinkBuilder
:
Link link = linkTo(methodOn(DoctorController.class).getDoctorById(id)).withSelfRel();
Aquí, usamos el enfoque más programático para crear vínculos. Apunta al getDoctorById()
método dentro de la DoctorController
clase. Como apunta a sí mismo, usamos el withSelfRel()
método para especificar la relación.
Alternativamente, podríamos haber usado el withRel()
método y pasado un String con una relación diferente.
Spring HATEOAS traducirá los detalles del punto final de la clase del controlador y el método que hemos proporcionado a WebMvcLinkBuilder
. La salida de este Link
objeto será exactamente la misma que la generada en el ejemplo anterior.
Vínculos relacionales
Para crear enlaces para recursos que tienen una relación entre ellos o apuntan a un recurso diferente, usaríamos el withRel()
método. Con esto podemos especificar el punto final con el que se puede acceder al recurso vinculado:
Link link = linkTo(methodOn(DoctorController.class)
.getDoctorPatients(doctor.getId()))
.withRel("patientList");
El fragmento de código anterior especifica que el usuario puede obtener el patientList
para el doctor
objeto, utilizando el getDoctorPatients()
método dentro de la DoctorController
clase. Cuando se agrega al cuerpo de la respuesta, genera el siguiente enlace:
"_links": {
"patientList": {
"href": "http://localhost:8080/doctors/1/patients"
}
}
Tenga en cuenta que no proporcionamos ninguna URL al crear el enlace. Spring HATEOAS puede extraer la información del creador de enlaces y generar una URL basada en las asignaciones que hemos utilizado.
Configuración
Para representar correctamente diferentes RepresentationModel
subtipos, puede habilitar la representación de hipermedios utilizando la @EnableHypermediaSupport
anotación. Puede pasar el HypermediaType
como un argumento a esta anotación, lo que le permite especificar el tipo de hipermedia, como JSON, UBER , HAL , etc. El uso de la anotación permite a Spring configurar los módulos de Jackson necesarios para representar hipermedia correctamente.
Normalmente, Spring detectará la pila de tecnología que está utilizando y ajustará automáticamente la configuración cuando agregue la anotación. Sin embargo, si tiene algunos requisitos personalizados, le sugerimos que consulte la documentación oficial .
Aplicación de demostración
Con todo lo dicho, escribamos una aplicación Spring simple con soporte de HATEOAS yendo a Spring Initializr y generando una aplicación Spring Boot en blanco con la dependencia Spring HATEOAS
( spring-boot-hateoas-starter
):
Crear un recurso
Para cualquier recurso que se exponga a través de la API REST, debe extenderse RepresentationModel
. Al extender la RepresentationModel
clase, también heredamos el add()
método, que se utiliza para adjuntar enlaces.
Creemos un modelo para Doctor
:
public class Doctor extends RepresentationModel<Doctor> {
private int id;
private String name;
private String speciality;
private List<Patient> patientList;
}
Como la Doctor
clase tiene una relación con los pacientes, creemos también el Patient
modelo:
public class Patient extends RepresentationModel<Patient> {
private int id;
private String name;
}
A continuación, en un controlador, en nuestro caso a DoctorController
, conectaremos automáticamente DoctorService
:
@RestController
@RequestMapping(value = "/doctors")
public class DoctorController {
@Autowired
DoctorService doctorService;
}
Como es de esperar, que contiene métodos tales como getDoctor()
, getDoctorWithPatients()
, getDoctors()
, etc, que todo cambio un Doctor
o una List<Doctor>
. La implementación se omite por brevedad; si desea echar un vistazo, el código está en GitHub .
Con esto, hemos creado un recurso. Al recuperar recursos, esperaremos un solo recurso o una colección de recursos. Como se indicó anteriormente, los envolveremos en un EntityModel
o CollectionModel
, respectivamente.
Recuperar un solo recurso
Primero implementemos la funcionalidad de buscar un solo médico. Como esperamos que la llamada a la API devuelva un solo recurso, envolveremos nuestra respuesta dentro de una EntityModel
clase:
@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);
}
Después de recuperar el Doctor
objeto, recorremos la lista de pacientes asociados y agregamos un enlace para cada uno de ellos. Cada uno de estos enlaces se puede utilizar para que cada individuo Patient
acceda a PatientController
.
De manera similar, estamos agregando un self
enlace al Doctor
que se usó para realizar las llamadas a la API. Junto con el autoenlace también estamos agregando un enlace relacional, apuntando a la lista de pacientes.
Al final del método, hemos envuelto nuestro Doctor
objeto en una EntityModel
clase y esto EntityModel
se devuelve como respuesta:
{
"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 / pacientes "}}}
“Dr. Sanders” tiene a “J. Smalling” y “Samantha Williams” como sus pacientes, y tanto el criterio de valoración del médico como el criterio de valoración de una lista de pacientes del médico se agregan a la respuesta, lo que la enriquece con recursos.
Recuperar varios recursos
Creemos otra llamada GET que devuelva todos los médicos disponibles en el sistema. Ahora que la respuesta que esperamos será una colección de Doctor
objetos, envolveremos la respuesta dentro del 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);
}
En este método, junto con el self
enlace para la llamada REST en sí, también estamos agregando un enlace propio para recuperar a cada médico individual. Cada médico tiene un vínculo relacional, que apunta a los pacientes asociados. Dentro de la lista de pacientes, cada paciente también tiene un self
enlace, que también se puede utilizar para recuperar al paciente específico.
Una vez que se agregan todos los enlaces, hemos envuelto la colección de Doctor
objetos dentro de un CollectionModel
y lo devolvió:
{
"_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"
}
}
}
Como puede ver en el resultado, con solo hacer una sola llamada, el usuario puede descubrir información adicional, que de otro modo no estaría presente.
Conclusión
Spring HATEOAS proporciona las bibliotecas y la infraestructura necesarias para implementar la arquitectura HATEOAS en aplicaciones basadas en Spring.
Como se desprende de los resultados, los usuarios pueden descubrir información adicional a partir de una sola llamada REST. Con esta información, es más fácil crear clientes REST dinámicos.
En este artículo, discutimos cómo funciona HATEOAS, la implementación de Spring y terminamos con la construcción de una aplicación simple para demostrar los conceptos.
El código fuente del código de muestra se puede encontrar aquí en GitHub.