Pollito Blog
January 8, 2024

Desarrollo basado en contratos 5: Las validaciones en el controlador no están funcionando... ¿Por qué?

Posted on January 8, 2024  •  10 minutes  • 1960 words  • Other languages:  English

Parchando validaciones javax obsoletas en Spring Boot 3.

Consulta el repositorio de github

Esta es una continuación de Desarrollo basado en contratos 4: Generando interfaces para controladores .

Todo lo que haremos aquí, lo puedes encontrar en el repositorio de github.

Spring City Explorer - Backend: Branch feature/cdd-5

Cambios en la especificación openAPI.yaml

{
  "timestamp": {
    "offset": {
      "totalSeconds": 0,
      "id": "Z",
      "rules": {
        "fixedOffset": true,
        "transitions": [],
        "transitionRules": []
      }
    },
    "year": 2024,
    "monthValue": 1,
    "dayOfMonth": 3,
    "hour": 18,
    "minute": 31,
    "second": 48,
    "nano": 218000000,
    "dayOfWeek": "WEDNESDAY",
    "dayOfYear": 3,
    "month": "JANUARY"
  }
}

Esto puede causar problemas de serialización en quien consume nuestro servicio.

Escribamos algunas pruebas unitarias para nuestros controladores

Aquí hay un ejemplo de cómo probar ArticleController.

@ExtendWith(MockitoExtension.class)
class ArticleControllerTest {
  @InjectMocks private ArticleController articleController;
  @Mock private ArticleService articleService;

  @Test
  void whenGetArticlesByCountryThenReturnsArticles() {
    ResponseEntity<Articles> expectedResponse = ResponseEntity.ok(mockArticles());
    when(articleService.getArticlesByCountry(anyString(), anyInt(), anyInt()))
        .thenReturn(expectedResponse.getBody());

    ResponseEntity<Articles> actualResponse =
        articleController.getArticlesByCountry(MOCK_STRING, 0, 0);

    assertEquals(expectedResponse.getBody(), actualResponse.getBody());
  }
}

Para que esto funcione, es necesario:

public interface ArticleService {
  Articles getArticlesByCountry(String country, Integer limit, Integer offset);
}
@Service
public class ArticleServiceImpl implements ArticleService {
  @Override
  public Articles getArticlesByCountry(String country, Integer limit, Integer offset) {
    return null;
  }
}
@RestController
@RequiredArgsConstructor
public class ArticleController implements ArticleApi {

  private final ArticleService articleService;

  @Override
  public ResponseEntity<Articles> getArticlesByCountry(
      String country, Integer limit, Integer offset) {
    return ResponseEntity.ok(articleService.getArticlesByCountry(country, limit, offset));
  }
}

Genial, escribamos una prueba fallida… ¿Por qué no falla?

Cambiemos rápidamente una línea en ArticleControllerTest

ResponseEntity<Articles> actualResponse =
        articleController.getArticlesByCountry(MOCK_STRING, 100, 0);

En nuestra especificación, indicamos que limit tiene un máximo de 10, por lo que seguramente 100 debería generar una excepción, ¿verdad?… Prueba aprobada.

Bueno, seguro que se trata de algunas cosas de Mockito que no se ejecutan correctamente. Simplemente ejecutemos la aplicación y realicemos un cURL request.

curl --location 'http://localhost:8080/article?limit=101'

Recibí 200 OK. Entonces, ¿quién tiene la culpa? Respuesta corta, el plugin, porque está desactualizado para los estándares actuales. Respuesta larga y cómo solucionarlo, sigue leyendo.

Un poco de historia: javax, jakarta y Spring Boot 3

Le pregunté a chatGPT:

explica sin muchos detalles técnicos cuál es el problema con los paquetes javax y jakarta, centrándose en lo que hacen las bibliotecas y por qué pasar de javax a jakarta

Y obtuve esto:

Paquetes javax

Transición a Jakarta

Paquetes de Jakarta

Spring Boot 3 elimina javax a favor de jakarta

Este cambio fue impulsado por el traslado de Java EE a Jakarta EE bajo la Fundación Eclipse, lo que llevó al cambio de nombre de los paquetes de javax a jakarta.

Para Spring Boot 3, estos son los puntos clave relacionados con la compatibilidad con los paquetes javax:

¿Qué tiene eso que ver con que las validaciones del controlador no funcionan entonces?

Bueno, lamentablemente el complemento solo puede generar código en la forma anterior a Spring Boot 3, usando javax. Podemos verificar que ingresando a la interfaz que nuestro controlador extiende y leyendo las importaciones. Encontraremos:

import javax.validation.Valid;
import javax.validation.constraints.*;

Entonces, lo que está sucediendo es que nuestra aplicación Spring Boot 3 simplemente ignora la validación javax, lo que resulta en nuestro comportamiento actual.

¿Entonces, cuales son nuestras opciones?

Pros y contras de la solución parche elegida

Ventaja: Ahora que estamos escribiendo nuestras propias validaciones, podemos incluso mejorar cosas en las que la OAS se queda corta

Si bien la OAS proporciona un marco sólido para validaciones de API estándar, a veces puede fallar en el manejo de escenarios de validación complejos o únicos que son específicos de cierta lógica de negocios o formatos de datos. Al escribir nuestras propias validaciones, podemos introducir un nivel de especificidad y flexibilidad que la OAS quizás no apoye inherentemente.

Este enfoque permite un control más granular sobre la integridad de los datos y el comportamiento de la API, asegurando que se alinee con mayor precisión con los requisitos de la aplicación y las expectativas del usuario. Además, las validaciones personalizadas también pueden servir como un medio para introducir controles de seguridad adicionales o hacer cumplir ciertas mejores prácticas que están más allá del alcance de la OAS, mejorando así la solidez y confiabilidad general de la API.

Pro: Es el menos perjudicial para la situación actual

La opción de implementar validaciones personalizadas a menudo surge como una solución altamente eficiente y mínimamente disruptiva, especialmente en comparación con medidas más drásticas como alterar las bibliotecas existentes.

Desventaja: Estamos haciendo un trabajo manual que es propenso a caer en obsolescencia

Imagine que los requisitos cambian y el arquitecto o usted crea yourServiceOAS_V2.yaml. Ahora no se trata solo de reemplazar, construir y probar. Ahora tienes que implementar cambios manualmente.

Hacer las cosas manualmente implica que existe la posibilidad de perder algo en el camino. Los desarrolladores podemos y cometeremos errores. Si algo se puede automatizar para evitar errores humanos evitables, es bueno pensar un poco en ello.

Desventaja: cuanto más lejos se produce el error, más difícil es mapear su estado de respuesta HTTP

Darle al servicio la responsabilidad de validar las entradas de las solicitudes no sigue el “principio de validación de entradas tempranas”. Si hay un error en la solicitud, se debe lanzar lo antes posible. En este caso, debería estar en el controlador.

Además esto plantea un nuevo problema: ahora que el error está en el servicio, ¿a qué estado lo asigno? Uno pensaría “fácil 400-algo”. ¿Pero cómo puedes estar tan seguro?

Creo que esta es la mayor desventaja de todas. Lo dejaré pasar por el momento, pero la solución ideal es no tener ningún problema para empezar, por lo que buscaremos un plugin mejor en el futuro.

Implementando la solución

Agregar jakarta en pom.xml

<!--
  It integrates the Hibernate Validator and the Validation API, providing a seamless experience for adding validation capabilities to Spring Boot applications.
  The specified version ensures compatibility with other Spring Boot 3.x components
-->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-validation</artifactId>
  <version>3.1.2</version>
  <scope>compile</scope>
</dependency>

Agregar anotaciones de jakarta a la interfaz de servicio.

import dev.pollito.springcityexplorer.annotation.ValidArticleCountry;
import dev.pollito.springcityexplorer.models.Articles;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import org.springframework.validation.annotation.Validated;

@Validated
public interface ArticleService {
  Articles getArticlesByCountry(
      @ValidArticleCountry String country,
      @Min(1) @Max(10) Integer limit,
      @Min(0) @Max(10000) Integer offset);
}

Nótese que:

Ejecute y vea cómo funciona.

Request

curl --location 'http://localhost:8080/article?country=asd'

Respuesta: en este momento se está asignando a 500, como lo haría cualquier ConstraintViolationException en un servicio. No me preocuparé mucho por esto ahora.

{
  "timestamp": "2024-01-09T12:34:31.755+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "trace": "all the trace exception long long text",
  "message": "getArticlesByCountry.country: Invalid country code",
  "path": "/article"
}

Baila y repite para el resto de los puntos finales. En POST /comment, tiene una clase como cuerpo de solicitud. Necesitará replicar esa clase en su código src con anotaciones de jakarta.

Otros cambios menores en pom xml

Próximos pasos

Hey, check me out!

You can find me here