Es muy común que en nuestros microservicios estén llenos de múltiples validaciones con bloques de if’s en los cuales probamos si el valor no es nulo, si tiene una longitud adecuada, fechas correctas, etc. y luego debemos informarle al usuario que fue lo que salió mal… y el 50% de nuestro código se basa en esas validaciones.
Afortunadamente entre las API’s de JavaEE/JakartaEE encontramos Bean Validation, que permite reducir todo ese código de una forma declarativa usando anotaciones y podemos aprovecharlas junto a MicroProfile para escribir microservicios que no solo comuniquen al usuario que ocurrió un error, si no que además indique que fué lo que salió mal y como puede corregirlo.
Todo el código que veremos a continuación se puede encontrar en el siguiente repositorio:
https://github.com/Motojo/MicroProfile-BeanValidation
Integrando Bean Validation a nuestro proyecto
Para integrar Bean Validation a nuestro proyecto, basta con agregar la dependencia a las API’s completas de JavaEE/JakartaEE o únicamente al proyecto de Bean Validation.
dependencies { /*--- Full JavaEE dependency or just javax.validation dependency ---*/ /* providedCompile group: 'javax', name: 'javaee-api', version: '8.0' */ providedCompile group: 'javax.validation', name: 'validation-api', version: '2.0.1.Final' providedCompile group: 'org.eclipse.microprofile', name: 'microprofile', version: '2.1' }
Ejemplo utilizando Gradle
Una vez integrado podemos empezar a utilizar las anotaciones en nuestros POJO’s según las validaciones que necesitemos, por ejemplo:
- @NotNull: Verifica que la propiedad no tenga valor null.
- @AssertTrue: Verifica que la propiedad tenga valor true.
- @Size: Verifica que la propiedad tenga un tamaño entre los valores min y max, aplica a Strings, Mapas, Colecciones y Arrays.
- @Min: Verifica que la propiedad tenga un valor numérico igual o mayor al especificado
- @Max:Verifica que la propiedad tenga un valor numérico igual o menor al especificado.
- @Email: Verifica que la propiedad sea un Email válido.
- @NotEmpty:Verifica que la propiedad no esté vacía, aplica a Strings, Colecciones, Mapas o Arrays.
- @NotBlank:Verifica que el texto no sea null o espacios en blanco.
- @Positive / @PositiveOrZero: Verifica que el valor numérico sea positivo incluyendo o no al cero.
- @Negative / @NegativeOrZero: Verifica que el valor numérico sea positivo incluyendo o no al cero.
- @Past / @PastOrPresent: Verifica que la fecha esté en el pasado incluyendo o no el presente.
- @Future / @FutureOrPresent: Verifica que la fecha esté en el futuro incluyendo o no el presente.
Y pueden utilizarse de la siguiente manera:
public class Book { private Long id; @NotNull @Size(min = 6) private String name; @NotNull private String author; @Min(6) @Max(200) @NotNull private Integer pages; }
Ejemplo de POJO anotado con Bean Validation
Integrando Bean Validation con JAX-RS
Una vez que Bean Validation se encuentra integrado en nuestro proyecto y nuestros objetos a validar se encuentran anotados, es momento de indicarle a nuestros microservicios que deben de realizar o no las validaciones correspondientes, y esto se hace de la siguiente manera utilizando la anotación @Valid:
@POST public Book createBook(@Valid Book book) { //Do something like saving to DB and then return the book with a new ID book.setId(20L); return book; }
Ejemplo de WebService validado por medio de Bean Validation
Al invocar nuestro servicio se realizarán las validaciones indicadas en el objeto Book y nos devolverá un error si algún parámetro es incorrecto o procederá a ejecutar el código de nuestro WebService si todo está correcto.
Ejemplo de WebService ejecutado correctamente
Ejemplo de WebService con errores (nombre del libro no enviado)
Mejorando el manejo de errores
Como pudimos observar en la imagen anterior, al existir un error el código del WebService no fué ejecutado y en lugar se envió la respuesta standard de error de nuestro servidor, sin embargo esto no es muy útil y puede causar aún más errores si quien consume el servicio espera que las respuestas estén en formato JSON.
Para mejorar nuestras respuestas e indicarle al usuario que fué lo que salió mal podemos escribir un pequeño interceptor del tipo ExceptionMapper<ConstraintViolationException> que transformará nuestra respuesta a JSON y brindará información más util al usuario para corregir su error.
@Provider public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> { private String getPropertyName(Path path) { //The path has the form of com.package.class.property //Split the path by the dot (.) and take the last segment. String[] split = path.toString().split("\\."); return split[split.length - 1]; } @Override public Response toResponse(ConstraintViolationException exception) { Map<String, String> errors = exception.getConstraintViolations().stream() .collect(Collectors.toMap(v -> getPropertyName(v.getPropertyPath()), ConstraintViolation::getMessage)); return Response.status(Response.Status.BAD_REQUEST) .entity(errors).type(MediaType.APPLICATION_JSON) .build(); } }
Interceptor para mejorar la respuesta estándar al ocurrir un error
Una vez escrito y registrado nuestro interceptor con la anotación @Provider al invocar nuestro servicio y si este tiene algún error en lugar de retornar la pantalla estándar de nuestro servidor nos retornará una respuesta similar a esta:
Ejemplo de WebService con errores utilizando el ExceptionMapper
Parametros en Query, Path y Header:
El uso de Bean Validation no se limita a objetos que se reciban como parte del cuerpo de la petición, también pueden ser aplicados a parámetros dentro de la URL como los Path y Query Params o validación de los headers, por ejemplo:
@GET public Book getBook(@Valid @NotNull @QueryParam("id") Long id) { //Just build a dummy book to return Book book = new Book(); book.setId(id); book.setAuthor("Jorge Cajas"); book.setPages(100); return book; }
Sin embargo al utilizar este servicio podemos notar que las respuestas en nuestro ExceptionMapper de errores ya no son tan útiles como esperábamos pues los nombres de los parámetros no son reconocidos automáticamente:
El error en el parámetro id no es reportado correctamente
Para corregir esto debemos de configurar a Bean Validation de tal forma que sepa de qué manera resolver los nombres de los parámetros de nuestros métodos JAX-RS.
El archivo validation.xml se utiliza para configurar varios aspectos de Bean Validation y debe colocarse en la carpeta de resources/META-INF. En este archivo colocaremos la propiedad <parameter-name-provider> con referencia a nuestra clase que se encargará de resolver los nombres de los parámetros de nuestros servicios JAX-RS.
<validation-config xmlns="http://jboss.org/xml/ns/javax/validation/configuration" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/configuration validation-configuration-1.1.xsd" version="1.1"> <parameter-name-provider>com.demo.validations.CustomParameterNameProvider</parameter-name-provider> </validation-config>
resources/META-INF/validation.xml
La clase CustomParameterNameProvider inspeccionará nuestros servicios JAX-RS, obtendrá las anotaciones de los parámetros de Query, Path o Header y completará la información necesaria para que Bean Validation sepa cómo completar los nombres en nuestro ExceptionMapper que escribimos anteriormente.
public class CustomParameterNameProvider implements ParameterNameProvider { @Override public List<String> getParameterNames(Constructor<?> constructor){ return lookupParameterNames(constructor.getParameterAnnotations()); } @Override public List<String> getParameterNames(Method method) { return lookupParameterNames(method.getParameterAnnotations()); } private List<String> lookupParameterNames(Annotation[][] annotations) { final List<String> names = new ArrayList<>(); if (annotations != null) { for (Annotation[] annotation : annotations) { String annotationValue = null; for (Annotation ann : annotation) { annotationValue = getAnnotationValue(ann); if (annotationValue != null) { break; } } // if no matching annotation, must be the request body if (annotationValue == null) { annotationValue = "requestBody"; } names.add(annotationValue); } } return names; } private static String getAnnotationValue(Annotation annotation) { if (annotation instanceof HeaderParam) { return ((HeaderParam) annotation).value(); } else if (annotation instanceof PathParam) { return ((PathParam) annotation).value(); } else if (annotation instanceof QueryParam) { return ((QueryParam) annotation).value(); } return null; } }
CustomParameterNameProvider.java
Una vez configurado, podemos volver a invocar nuestro servicio y la respuesta deberá ser similar a esta:
El query param id es reportado con error con el nombre correcto.