Validate Your Microservices With MicroProfile and Bean Validation
Why and how
Join the DZone community and get the full member experience.
Join For FreeIs a common code practice in our microservices to have a lot of If’s blocks to validate if some values are null, or if they have the right size, the dates are correct, etc. then the developer needs to inform the user that something went wrong and 50% of our code are those validations.
Fortunately, within the JavaEE/JakartaEE API’s there is Bean Validation that allows the developer to reduce all of that validation code in a declarative way using annotations and can be used along with MicroProfile to write better microservices that not only informs the users that something went wrong but also let them know how to fix it.
All the code used in this post can be found at this repository:
https://github.com/Motojo/MicroProfile-BeanValidation
Integrating Bean Validation in our project
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'
}
Then the developer can start using the Bean Validation annotations on any POJO, for example:
- @NotNull: Verifies that the value is not null.
- @AssertTrue: Verifies that the value is true.
- @Size: Verifies that the size of the value is between the min and max specified, can be used with Strings, Maps, Collections, and Arrays.
- @Min: Verifies that the numeric value is equal or greater than the specified value.
- @Max: Verifies that the numeric value is equal or lower than the specified value.
- @Email: Verifies that the value is a valid email.
- @NotEmpty: Verifies that the value is not empu, applies to Strings, Collections, Maps, and Arrays.
- @NotBlank: Verifies that the text is not whitespaces.
- @Positive / @PositiveOrZero: Verifies that the numeric value is positive including or not the zero.
- @Negative / @NegativeOrZero: Verifies that the numeric value is negative including or not the zero.
- @Past / @PastOrPresent: Verifies that the date is in the past including or not the present.
- @Future / @FutureOrPresent: Verifies that the date is in the future including or not the present.
Can be used like this:
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;
}
Integrating Bean Validation With JAX-RS
When Bean Validation is integrated into the project and POJOS are annotated is time to tell to JAX-RS endpoints to make or not the validations using the annotation @Valid like this:
@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;
}
Example of JAX-RS endpoint annotated to validate the Book Object
When calling the service all the validation on the Book objects will be executed and an error will be returned if something went wrong or if everything is ok the service code will be executed.
Better Error Management
As shown in the previous image, when there is an error the service code was not executed and the standard error response of the server is sent, but this is not very useful at all and can be a cause of more errors if the client expects that all the responses will be returned in JSON format.
To improve the error responses and let the user know what happened an interceptor of type ExceptionMapper
can be written to transform the standard response into JSON and add more useful information to it.
@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();
}
}
Once the exception mapper is registered using the @Provider annotation and calling the service if there is an error response like this will be returned:
Query, Path, and Header Params
Bean Validation is not limited to be used in request objects, also can be used on Query, Path and Header params like this:
@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;
}
But calling this service, at the error response we can be noted that the ExceptionMapper
previously wrote is not useful at all because the parameter name was not resolved.
To fix this Bean Validation must be configured in a way it knows how to resolve the JAX-RS method’s parameter names.
The validation.xml is used to configure various aspects of Bean Validation and must be placed at resources/META-INF directory. On this file, we will use the <parameter-name-provider> property with a reference to a class that will be responsible to resolve the JAX-RS parameter names.
<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>
The class CustomParameterNameProvider will inspect the JAX-RS methods and from the annotations of Query, Path or Header params will complete the necessary information in order to resolve correctly the parameter names on the Exception Mapper previously written.
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;
}
}
Once Bean Validation is configured, a call to the service with error will be like this:
Further Reading
Published at DZone with permission of Jorge Cajas. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments