Constraint Validation in Spring Boot Microservices
In a microservice architecture, services may accept several, if not many, of the same inputs. This pattern can easily lead to code duplication and redundancy between services. In an effort to mitigate these drawbacks and keep service code focused, we can devise a robust solution involving several APIs provided by Spring and Java.
The following tutorial will assume some working knowledge of Java and Spring Boot, but will cater to a range of skill levels. Regardless, it never hurts to see another developer's code!
Our solution will involve combining the Java and Spring Boot APIs,
Hibernate Validator, enhanced as part of JSR 380, is a specification of the Java API for standard Bean validation. In the context of Spring Boot applications, you may have used this without thinking twice. Examples include:
In this tutorial, we will examine how to go beyond these basic validations using the
ConstraintValidator interface to define our own set of constraints.
While other forms of exception handling exist within the Spring ecosystem, the
ResponseEntityExceptionHandler provides global (and centralized) exception handling within a service. This globalization is key to the efficacy of our custom constraint annotation since it allows us to validate multiple Beans (or fields within them). That said, we will investigate how we can leverage this class to gracefully handle violations to our constraints.
Let's dive in. To avoid bloating the tutorial with boilerplate code, you will only find necessary blocks of code in the sections below. This article is coupled with a working example on GitHub.
Note: I will be making references to this example project throughout the tutorial.
The list is short and sweet:
Creating the Annotations
We'll start simple. Let's say we've implemented a Fridge and Pantry Service of which allows us to:
- Manage the Fridge and Pantry repositories
- Accept POST and/or PUT requests with a JSON payload
We want to validate common fields between request models of both services. Our request model may look something like this:
One of the simplest constraints we can build will involve composing existing constraints, such as
@Max in the example above. This allows us to put an explicit label on common constraints and call it "business logic". Below, we define
There's a lot going on here, so let's break this down:
@Constraintmarks an annotation as being a Bean Validation constraint and allows us to specify
ConstraintValidatorimplementations; zero, one, or many implementations are welcome here
@Retentionis set such that our annotation will be retained at runtime
@Targetis set such that we can validate different types of inputs to our services
payloadare required by
@Constraintbut do not have to be set--these provide specificity beyond what we'll cover today
By no means is this a simplification. Looking past the verbosity, the annotation opens quite a few doors to make handling complexity a breeze as we'll see in the next example.
Let's define a constraint for the category field such that:
- Category must be passed and cannot be empty
- Only certain categories are allowed to be passed
- Categories may differ between the Fridge and Pantry services
To implement this annotation, we will expand upon the premise of our first annotation by adding a custom parameter and providing an implementation of the
ConstraintValidator interface. The result looks something like this:
A few more things are happening in this annotation compared to
@FoodQuantity. We've specified a new parameter,
allowed, to restrict what may be passed into
category. Notice the default value--this array is only referenced if values are not passed into
@FoodCategory. To handle this constraint, we've implemented
Let's breakdown our new validator class:
ConstraintValidatoris parameterized with the annotation class and the type being validated--a
Stringcontaining the value of category
A global field
allowed, set within the overridden
- It is within this method that we gain access to the parameters of
@FoodCategoryfor use throughout the validator class
- It is within this method that we gain access to the parameters of
isValidis the meat and bones of our constraint validation
- For invalid scenarios, we disable the default constraint violation, build a proper error message, and return false--this eventually throws an exception we'll be interested in later
Lastly, to get the most out of our annotation, we'll propagate the category field into two subclasses pertaining to each service.
After all our hard work, we've reached a clean set of request models ready to be bombarded with invalid values:
Handling Validation Errors
Until now, we've only defined constraints we (and our consumers) must follow. Let's open up an endpoint to allow food to be added to the fridge and test our constraints:
There are a few critical aspects to note for our annotations to work properly:
@Validatedmust be used either at the class or method level to indicate where validation needs to take place
@Validis used to mark a property for validation cascading, which triggers our constraints
Let's send a payload:
Success! But let's see what happens when we send another payload we know will result in error:
Note we are violating multiple constraints for this request model. You should see a verbose response containing the details of the errors encountered and the exceptions thrown. This verbosity isn't ideal for us (or our consumers) to deal with, so let's filter out important details with a simple override of the
A further look into the error response provided by Spring, you may notice the exception thrown:
MethodArgumentNotValidException. This is the exception we will be interested in for handling constraint violations within the exception handler.
First, we need a model to capture relevant information. We're able to distill the following from the original Spring response:
Next, we'll construct both the global exception handler and a method to handle our constraints:
Let's note a few things here:
- Explore what might differ for violating constraints of
@RestControllerAdviceis just that--a specialized component for classes that declare `@ExceptionHandler methods shared across multiple controller classes
We override the
handleMethodArgumentNotValidmethod so we may
- Log important information
- Build a small, focused error response
- Return an HTTP status code of our choice, based on the constraint violated
Upon sending a payload that violates the constraints we've defined, you should see a succinct response indicating where we went wrong:
This tutorial provides some basic forms of constraint validation within a Spring-/Java-based REST service. If you are looking to take things a bit further, here are a few places you can start:
- Implement nested constraints within a complex request model
- Increase the flexibility of the HTTP status code returned to the consumer
- Expand the sample project to handle the nuance of a composite service--a Picnic Service, for instance
- Explore the
@ConstraintAPI further--what might
groupsbe used for?
This concludes the tutorial on implementing custom constraint validators using annotations! Don't be afraid to let me know if I missed anything. I certainly welcome (and appreciate) criticism, questions, and the like.
For further reference, here is the GitHub repository with the full working code and examples presented in this article.