Pollito Blog
March 18, 2024

Manifiesto de Pollito sobre el desarrollo basado en contratos de Java Spring Boot para microservicios 2

Posted on March 18, 2024  •  11 minutes  • 2196 words  • Other languages:  English

Esta es una continuación de Manifiesto de Pollito sobre el desarrollo basado en contratos de Java Spring Boot para microservicios 1 .

openapi-generator-maven-plugin

Antes de entrar en la charla técnica, permítanme presentarles el componente principal que ayudará a adoptar las prácticas de desarrollo impulsado por contratos: la dependencia org.openapitools » openapi-generator-maven-plugin » 7.4.0 .

openapi-generator-maven-plugin.png

Es un potente complemento de Maven para proyectos Java que automatiza la generación de clientes API, modelos, y documentación a partir de archivos OAS.

springBootStarterTemplate

springBootStarterTemplate es un punto de partida para proyectos futuros, diseñado para adoptar prácticas de desarrollo impulsado por contratos (CDD). Encapsula dependencias esenciales y estándares de mejores prácticas.

Tiene tres ramas:

springBootStarterTemplate -> feature/provider-gen

Hagamos una revisión del contenido de cada archivo relevante.

filetree

aspect.LoggingAspect

Lógica de registro centralizada de toda la aplicación, particularmente para los métodos del controlador.

config.LogFilterConfig

Clase de configuración en una aplicación Spring Boot, dedicada a configurar un filtro personalizado, específicamente filter.LogFilter.

De forma predeterminada, garantiza que LogFilter se aplique globalmente a todas las solicitudes.

config.WebConfig

Clase de configuración enfocada en Cross-Origin Resource Sharing (CORS) settings .

De forma predeterminada, permite solicitudes de origen cruzado desde cualquier fuente, lo cual es bueno para fines de desarrollo. Sin embargo, es posible que esto no sea adecuado para un entorno de producción.

controller.advice.GlobalControllerAdvice

Manejador de excepciones globales diseñado para detectar y manejar diversas excepciones que pueden ocurrir durante el procesamiento de solicitudes web.

De forma predeterminada, maneja:

Cada método de controlador devuelve ResponseEntity<Error>, donde Error debe ser un esquema en el archivo OAS donde se hace el rol de proveedor.

De forma predeterminada, también contiene la anotación @Order() vacía sin argumentos. Esto significa que el manejador se ejecuta al final en relación con otros componentes de @ControllerAdvice.

filter.LogFilter

Implementa un filtro de servlet para registrar los detalles de la solicitud y la respuesta.

util.Constants

Contenedor para constantes de toda la aplicación.

De forma predeterminada, solo contiene la constante SLF4J_MDC_SESSION_ID_KEY, utilizada en LogFilter como clave en el contexto de diagnóstico asignado (MDC) SLF4J para almacenar y recuperar un identificador de sesión único.

Siéntase libre de agregar aquí todas las constantes necesarias.

util.ErrorResponseBuilder

Utilidad para construir entidades de respuesta a errores, de modo que todas las respuestas a errores tengan la misma estructura.

SpringBootStarterTemplateApplication

Punto de entrada predeterminado a una aplicación Spring Boot.

resources > openapi files

Esta es una carpeta totalmente arbitraria dentro de los recursos donde decidí que es lo suficientemente buena para colocar todos los archivos OAS.

Es importante recordar que en la definición de contrato que hice, debemos cumplir con entradas, salidas y errores.

Los microservicios deben cumplir con un contrato, que define entradas, salidas y errores.

Por lo tanto, cada contrato debe tener un esquema similar a Error (no es necesario que se llame Error), que, después de las tarea de generate, se utilizará para construir respuestas de error estandarizadas.

Aquí está mi esquema de error recomendado:

Error:
  type: object
  properties:
    timestamp:
      type: string
      description: The date and time when the error occurred in ISO 8601 format.
      format: date-time
      example: "2024-01-04T15:30:00Z"
    session:
      type: string
      format: uuid
      description: A unique UUID for the session instance where the error happened, useful for tracking and debugging purposes.
    error:
      type: string
      description: A brief error message or identifier.
    message:
      type: string
      description: A detailed error message.
    path:
      type: string
      description: The path that resulted in the error.

No dudes en organizar archivos en subcarpetas si es necesario. No olvide mantener la coherencia en pom.xml (más sobre esto más adelante).

resources > logback.xml

Configuración para registrar información en la consola, con un patrón personalizado que incluye un ID de sesión para una mejor trazabilidad de las entradas del registro.

pom.xml

Este es un ejemplo de cómo configurarlo para la generación de proveedores:

<plugin>
  <groupId>org.openapitools</groupId>
  <artifactId>openapi-generator-maven-plugin</artifactId>
  <version>7.2.0</version>
  <executions>
      <execution>
          <id>provider generation - your OAS file</id>
          <goals>
              <goal>generate</goal>
          </goals>
          <configuration>
              <inputSpec>${project.basedir}/src/main/resources/openapi/your OAS file</inputSpec>
              <generatorName>spring</generatorName>
              <output>${project.build.directory}/generated-sources/openapi/</output>
              <apiPackage>dev.pollito.springbootstartertemplate.api</apiPackage>
              <modelPackage>dev.pollito.springbootstartertemplate.models</modelPackage>
              <configOptions>
                  <interfaceOnly>true</interfaceOnly>
                  <useSpringBoot3>true</useSpringBoot3>
                  <useEnumCaseInsensitive>true</useEnumCaseInsensitive>
              </configOptions>
          </configuration>
      </execution>
  </executions>
</plugin>

Veamos qué está pasando aquí.

Diagrama UML

uml

Cómo utilizar esta plantilla

  1. Clonar el repositorio y eliminar su relación con el repositorio original.
  1. Agregue un archivo OAS en recursos/openapi.
  1. En pom.xml, descomente el código de bloque relacionado con openapi-generator-maven-plugin.
  2. Edite el código de bloque para que apunte a su archivo OAS agregado recientemente en resources/api.
  1. maven clean + maven compile.
  2. Cree un @RestController e implemente la interfaz generada.
  3. Ejecute la aplicación.

Ejemplo de implementación

Puede encontrar este código en feature/provider-gen-example

0. Clonar el repositorio y eliminar su relación con el repositorio original

Sigue estos pasos

git clone https://github.com/franBec/springBootStarterTemplate.git
cd springBootStarterTemplate
rm -rf .git
git init

Ahora tienes un repositorio limpio totalmente nuevo.

Opcional pero recomendado dale tu propia identidad al repositorio. Para esto:

Agregue un archivo OAS en recursos/openapi

Para esto, uniré el ejemplo de tienda de mascotas de The OpenAPI Specification github .

No olvide agregar (o en este caso reemplazar el esquema de error ya existente) en la OEA con el esquema de error recomendado en este blog.

2. En pom.xml, descomente el código de bloque relacionado con openapi-generator-maven-plugin

Esto es fácil, simplemente elimine el <— —> que rodea el bloque de código.

3. Edite el código de bloque para que apunte a su archivo OAS agregado recientemente en resources/api

Aquí está cómo se ve después de editar:

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.2.0</version>
    <executions>
        <execution>
            <id>provider generation - petstore.yaml</id>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <inputSpec>${project.basedir}/src/main/resources/openapi/petstore.yaml</inputSpec>
                <generatorName>spring</generatorName>
                <output>${project.build.directory}/generated-sources/openapi/</output>
                <apiPackage>io.swagger.petstore.api</apiPackage>
                <modelPackage>io.swagger.petstore.models</modelPackage>
                <configOptions>
                    <interfaceOnly>true</interfaceOnly>
                    <useSpringBoot3>true</useSpringBoot3>
                    <useEnumCaseInsensitive>true</useEnumCaseInsensitive>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

Edité los valores de las etiquetas apiPackage y modelPackage, para que aparezca el error de compilación y mostrar cómo solucionarlo.

4. maven clean + maven compile

Como se dijo, obtenemos package dev.pollito.springbootstartertemplate.models does not exist compile errors

Arreglemos esto. En cada archivo, cambie la importación rota. Asegúrese de importar el que generó el complemento. Error es un nombre muy común, podrías importar el incorrecto.

En este caso el que necesitamos es el segundo, de io.swagger.petstore.models

compile errors

Después de corregir las importaciones, intente compilar nuevamente. Deberíamos estar listos para partir.

5. Cree un @RestController e implemente la interfaz generada.

Me gusta mantener una cohesión entre el nombre que se le dio a la interfaz cuando se generó y el nombre de mi clase de controlador. Entonces, veamos en las fuentes generadas qué nombre se le dio a la interfaz en la generación.

generated interface

Allí podemos ver que se llamaba PetsApi, así que creemos PetsController:

@RestController
public class PetsController implements PetsApi {
}

En intelliJ IDEA, si presiona Ctrl+O dentro de una clase que implementa algo, aparecerá una ventana emergente que le preguntará qué métodos de interfaz desea implementar.

Aquí selecciona todo lo que necesitas. Por lo general, no necesitará nada de java.lang.Object y tampoco necesitará el método que devuelve Optional NativeWebRequest.

ctrl+O

Ahora tenemos todo este código:

@RestController
public class PetsController implements PetsApi {
    @Override
    public ResponseEntity<Void> createPets(Pet pet) {
        return PetsApi.super.createPets(pet);
    }

    @Override
    public ResponseEntity<List<Pet>> listPets(Integer limit) {
        return PetsApi.super.listPets(limit);
    }

    @Override
    public ResponseEntity<Pet> showPetById(String petId) {
        return PetsApi.super.showPetById(petId);
    }
}

6. Ejecute la aplicación

Pruebe esta solicitud:

curl --location 'http://localhost:8080/pets'

Recibirá un cuerpo vacío con un estado 501 Not Implemented.

Y los registros muestran la siguiente información:

2024-03-19 12:20:04 INFO  d.p.s.filter.LogFilter [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - >>>> Method: GET; URI: /pets; QueryString: null; Headers: {user-agent: PostmanRuntime/7.37.0, accept: */*, cache-control: no-cache, postman-token: 3c483439-921c-48d5-ab28-a85d76fce2e8, host: localhost:8080, accept-encoding: gzip, deflate, br, connection: keep-alive}
2024-03-19 12:20:04 INFO  d.p.s.aspect.LoggingAspect [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - [PetsController.listPets(..)] Args: [null]
2024-03-19 12:20:05 INFO  d.p.s.aspect.LoggingAspect [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - [PetsController.listPets(..)] Response: <501 NOT_IMPLEMENTED Not Implemented,[]>
2024-03-19 12:20:05 INFO  d.p.s.filter.LogFilter [SessionID: 433cd727-56f4-4236-9cf7-04434f54c7c4] - <<<< Response Status: 501

Siguiente lectura

Pollito’s Manifest on Java Spring Boot Contract-Driven Development for microservices 3

Hey, check me out!

You can find me here