Spring HATEOAS: servicios web RESTful impulsados ​​por hipermedia

Con HATEOAS, podemos exponer información adicional en una sola llamada a la API. En este artículo, construiremos un servicio web RESTful impulsado por hipermedia en Java y Spring HATEOAS.

Introducción

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 crean en torno a 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 objetivo 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 de conexión con una sola llamada. Esto permite a los usuarios construir sus sistemas con llamadas API dinámicas, moviéndose de un extremo a otro utilizando la información recuperada de cada llamada.

Para entender esto mejor, eche un vistazo a la siguiente respuesta de la API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
    "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 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 todos los pacientes de un solo médico.

Lo que tenemos aquí es una respuesta enriquecida con recursos, donde los enlaces proporcionados son recursos que enriquecen nuestra respuesta con información adicional.

Primavera HATEOAS

Primavera HATEOAS proporciona bibliotecas para implementar la arquitectura HATEOAS en una aplicación Spring con facilidad. Usando la API Spring HATEOAS, los enlaces se pueden crear y devolver como parte del objeto de respuesta de la API.

Dependencias Spring HATEOAS

Usando Maven, agregar Spring HATEOAS es tan fácil como incluir las dependencias:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<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>

Alternativamente, usando Gradle, puede agregar:

1
2
implementation 'org.springframework.plugin:spring-plugin-core:2.+'
implementation 'org.springframework.hateoas:spring-hateoas:1.+'

Dependencias Spring Boot HATEOAS

Aún más fácil, para las aplicaciones Spring Boot, puede usar la dependencia de Maven spring-boot-starter-hateoas:

1
2
3
4
5
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-hateoas</artifactId>
    <version>[2.2.4.RELEASE,)</version>
</dependency>

Del mismo modo, si está utilizando Gradle, simplemente puede agregar:

1
implementation 'org.springframework.boot:spring-boot-starter-hateoas:2.+'

El uso de la dependencia spring-boot-starter-hateoas incluye las dependencias spring-hateoas y spring-boot-starter-web, 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 Links y RepresentationModels (un contenedor para una colección de Links).

El RepresentationModel se amplía luego a EntityModel (para recursos únicos) y CollectionModel (para múltiples recursos), así como a PagedModel.

Tomemos un breve momento para explicar cada uno de estos antes de implementarlos en una demostración de trabajo.

Enlaces

El objeto Link 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í:

1
2
3
4
5
"_links": {
    "self": {
        "href": "http://localhost:8080/doctors/1"
    }
}

El enlace contiene un atributo href, que apunta al URI del recurso. El atributo href está envuelto dentro de una etiqueta self, que identifica la relación con la entidad. Esto significa que el recurso apunta a sí mismo, esencialmente.

¿Por qué el recurso se 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 por defecto.

Si luego queremos echar un vistazo a la lista de médicos, podemos navegar hasta ella a través del enlace.

Modelos de Representación

El RepresentationModel actúa como una clase raíz para todas las demás clases del modelo Spring HATEOAS. Contiene una colección de ‘Enlaces’ y proporciona un método para agregarlos/eliminarlos.

Crear su propio modelo es tan fácil como extender la clase RepresentationModel. De lo contrario, puede utilizar cualquiera de los modelos disponibles:

  • Modelo de Entidad: El EntityModel se usa para representar un recurso que corresponde a un solo objeto. Puede envolver su recurso con EntityModel 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 el Modelo paginado para representar dichos recursos.

Vamos a crear un recurso de muestra que extienda la clase RepresentationModel:

1
2
3
4
public class Doctor extends RepresentationModel<Doctor> {
    private int id;
    private List<Patient> patientList;
}

Por ahora, nuestro modelo Doctor solo tiene una propiedad id y una lista de pacientes. A continuación, agregaremos un Enlace al recurso, que apuntará el recurso a sí mismo.

Objeto de enlace

Los objetos Spring HATEOAS Link toman argumentos String para especificar el URI y la relación entre las entidades. Estos son básicamente los atributos href y rel:

1
2
3
4
Link selfLink = new Link("http://localhost:8080/doctors/1", "self");

Doctor doctor = new Doctor();
doctor.add(selfLink);

Cuando se devuelve un objeto médico (como se muestra en la aplicación de demostración en secciones posteriores), el cuerpo de la respuesta contendrá:

1
2
3
4
5
"_links": {
    "self": {
        "href": "http://localhost:8080/doctors/1"
    }
}
Constructor de enlaces MVC

Sin embargo, no se recomienda codificar valores en el constructor de la clase Link. Rápidamente se vuelve difícil administrarlos y actualizarlos a medida que crece su aplicación/API. Para combatir esto, podemos usar WebMvcLinkBuilder, que nos permite crear enlaces usando clases de controlador y apuntando a sus métodos.

Recreemos el enlace del ejemplo anterior usando WebMvcLinkBuilder:

1
Link link = linkTo(methodOn(DoctorController.class).getDoctorById(id)).withSelfRel();

Aquí, estamos usando el enfoque más programático para crear enlaces. Apunta al método getDoctorById() dentro de la clase DoctorController. Como se apunta a sí mismo, usamos el método withSelfRel() para especificar la relación.

Alternativamente, podríamos haber usado el método withRel() y pasar 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 objeto Link será exactamente la misma que la generada en el ejemplo anterior.

Enlaces relacionales {#enlaces relacionales}

Para crear enlaces para recursos que tienen una relación entre ellos o apuntan a un recurso diferente, usaríamos el método withRel(). Usando esto, podemos especificar el punto final con el que se puede acceder al recurso vinculado:

1
2
3
Link link = linkTo(methodOn(DoctorController.class)
                .getDoctorPatients(doctor.getId()))
                .withRel("patientList");

El fragmento de código anterior especifica que el usuario puede obtener patientList para el objeto doctor, utilizando el método getDoctorPatients() dentro de la clase DoctorController. Cuando se agrega al cuerpo de la respuesta, genera el siguiente enlace:

1
2
3
4
5
"_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 subtipos de RepresentationModel, puede habilitar la representación hipermedia utilizando la anotación @EnableHypermediaSupport. Puede pasar HypermediaType como 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 le permite a Spring configurar los módulos necesarios de Jackson para representar hipermedia correctamente.

Por lo general, 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] (https://docs.spring.io/spring-hateoas/docs/1.1.0.M2/reference/html/#configuration).

Aplicación de demostración

Dicho todo esto, escribamos una aplicación Spring simple con soporte HATEOAS yendo a Spring Inicializar y generando una aplicación Spring Boot en blanco con Spring HATEOAS (spring-boot-hateoas-starter) dependencia:

spring initializr blank project

Crear un recurso

Para cualquier recurso que se exponga a través de la API REST, debe extender RepresentationModel. Al extender la clase RepresentationModel, también heredamos el método add(), que se usa para adjuntar enlaces a él.

Vamos a crear un modelo para un Doctor:

1
2
3
4
5
6
public class Doctor extends RepresentationModel<Doctor> {
    private int id;
    private String name;
    private String speciality;
    private List<Patient> patientList;
}

Como la clase Doctor tiene una relación con los pacientes, creemos también el modelo Patient:

1
2
3
4
public class Patient extends RepresentationModel<Patient> {
    private int id;
    private String name;
}

A continuación, en un controlador, en nuestro caso un DoctorController, conectaremos automáticamente el DoctorService:

1
2
3
4
5
6
7
@RestController
@RequestMapping(value = "/doctors")
public class DoctorController {

    @Autowired
    DoctorService doctorService;
}

Como era de esperar, contiene métodos como getDoctor(), getDoctorWithPatients(), getDoctors(), etc., que devuelven un Doctor o List<Doctor>. La implementación se omite por razones de brevedad; si desea echar un vistazo, el código está disponible en GitHub.

Con esto, hemos creado un recurso. Al recuperar recursos, esperamos un solo recurso o una colección de recursos. Como se indicó anteriormente, los envolveremos en un EntityModel o CollectionModel, respectivamente.

Recuperación de un único recurso

Primero implementemos la funcionalidad de buscar un solo médico. Dado que esperamos que la llamada a la API devuelva un solo recurso, envolveremos nuestra respuesta dentro de una clase EntityModel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@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 objeto Doctor, recorremos la lista de pacientes asociados y agregamos un enlace para cada uno de ellos. Cada uno de estos enlaces se puede utilizar para obtener cada Patient individual a través del PatientController.

De manera similar, estamos agregando un enlace “auto” al “Doctor” que se usó para realizar las llamadas a la API. Junto con el autoenlace, también estamos agregando un enlace relacional, que apunta a la lista de pacientes.

Al final del método hemos envuelto nuestro objeto Doctor en una clase EntityModel y este EntityModel se devuelve como respuesta:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
    "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"
        }
    }
}

"Dr. Sanders" tiene "J. Smalling" y "Samantha Williams" como sus pacientes y tanto el criterio de valoración para el médico como el criterio de valoración para una lista de los pacientes del médico se agregan a la respuesta. - haciéndolo enriquecido en recursos.

Recuperación de múltiples recursos

Vamos a crear otra llamada GET que devuelva todos los médicos disponibles en el sistema. Ahora que la respuesta que esperamos será una colección de objetos Doctor, envolveremos la respuesta dentro de CollectionModel:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@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 enlace “auto” 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 enlace “auto”, que también se puede usar para recuperar al paciente específico.

Una vez que se agregaron todos los enlaces, envolvimos la colección de objetos Doctor dentro de un CollectionModel y lo devolvimos:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
{
    "_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 la salida, con solo hacer una 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 única 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 construyendo una aplicación simple para demostrar los conceptos.

El código fuente del código de muestra se puede encontrar aquí en GitHub.