In this article I will describe how to leverage Spring's Expression Language (SpEL) in order to write powerful class-level and cross-parameter constraints using the JSR 349 - (Bean Validation 1.1) reference implementation Hibernate Validator.
First of all, I will briefly introduce Hibernate Validator which is a great implementation of the JSR 349 standard. It can be used to validate your Java classes against certain constraints. Annotations such as @NotNull, @Size, @AssertTrue, @Past or @Future are typical examples for such constraints. These annotations allow you to separate the validation logic from your code using a declarative mechanism. However, once you are getting used to this approach you will soon realise that the standard annotations will not suffice to validate all your domain classes and services. But don't worry, Hibernate Validator allows you the writing of your own custom annotations which I will briefly explain in Part 1 of this article. After introducing the state-of-the art in validation, I will show you the limitations of the approach in Part 2 and present a flexible, powerful and still easy-to-implement strategy based on the Spring Expression Language to support class-level & cross-parameter constraints.
This annotation is targeted at fields or other annotations and the @Constraint annotation defines by which class the validation will be implemented. Furthermore, we define methods groups() for group-based validation (for further information on this topic, please refer to Chapter 5, Grouping constraints of the Hibernate Validator documentation) and message() / payload() for printing messages when validation fails. These are the standard methods supported by Hibernate Validator. Additionally, we define the min() method to allow the user to define the minimum age to be checked. Next, I will show how to implement age validation class containing the validation logic:
Once again a new validation implementation is needed:
}
You can extend this class to support other types of dates, such as java.util.Date or java.time.ZonedDateTime as well.
All classes you are planning to use our @ValidUsername annotation for must then implement the new UsernameProvider interface.
But what if we want to generalize our username validation strategy into a generic @NotAllAttributesNull constraint which checks whether at least one attribute of the annotated class is not null? We could use Object as the type parameter in the validation class and lookup all getter methods via reflection. However, the code based on reflection tends to get complex and difficult to read. Isn't there any better approach? Yes, for sure! Use Spring Expression Language to implement class-level and cross parameters constraints.
As you can see I've specified an extra attribute called value which allows us to define our concrete (Spring expression language, SpEL) expression. Now have a look on how simple it is to write the validator class based on SpEL:
This class is able to evaluate the SpEL expression given by the expression attribute of your annotation and returns the result as the result for your validation! Please make yourself a bit familiar with SpEL to understand how powerful and flexible the application of this approach is (please refer to the Spring docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html)
The starting point for your evaluation SpEL's #this variable pointing to the current evaluation object which is the object to be validated in our case. You can access all properties and invoke all methods of the object using SpEL.
Lets come back to our User class and apply the new validator for checking whether login and email are null.
@ValidateClassExpression(value = "!(#this.login == null && #this.email == null)", message = "Login or email must be defined.")
Great :-). And imagine user has another attribute which is collection of cars. How could we check that the user has at least one car? It is so easy:
@ValidateClassExpression(value = "!#this.cars.isEmpty()", message = "User must have at least one car.")
We could also combine several expressions by a simple conjunction of both expressions but the downside of this approach is that you could only use one validation message instead of a specific message for each constraint.
@ValidateClassExpression(value = "!(#this.login == null && #this.email == null) && !#this.cars.isEmpty()", message = "Validation failed")
No problem! Since Java 8 it is possible to make annotations repeatable. Thus, we can add the following annotation to the @ValidateClassExpression annotation:
In contrast to the class-level validation where we accessed only a single object (the one to be validated) we need to access the array of arguments from the method to be validated. For convenience we register the arguments as variables named arg0 - argN. We can now redefine our DatesValidator as follows:
How powerful and simple! In my opinion simplicity and productivity for writing constraints is inherently important because otherwise you might skip certain validation constraint because of the high implementation effort and this could lead to illegal states in the future.
I hope you found this article helpful and it inspired you to combine the Spring Expression language with Hibernate Validator in the future. The two annotations that we've implemented using SpEL @ValidClassExpression and @ValidParametersExpression can be used for all kinds of use cases related to class-level or cross-parameter validation. This allows you to apply validation to all your classes and methods which will definitely increase your code quality in the long run and helps you to separate your validation from your business code.
I am looking forward to any comments, improvements or extensions to the presented approach. Thanks for reading. You can checkout the code examples presented in this article from my Github repository: https://github.com/Javatar81/code-examples/tree/master/spelvalidation
First of all, I will briefly introduce Hibernate Validator which is a great implementation of the JSR 349 standard. It can be used to validate your Java classes against certain constraints. Annotations such as @NotNull, @Size, @AssertTrue, @Past or @Future are typical examples for such constraints. These annotations allow you to separate the validation logic from your code using a declarative mechanism. However, once you are getting used to this approach you will soon realise that the standard annotations will not suffice to validate all your domain classes and services. But don't worry, Hibernate Validator allows you the writing of your own custom annotations which I will briefly explain in Part 1 of this article. After introducing the state-of-the art in validation, I will show you the limitations of the approach in Part 2 and present a flexible, powerful and still easy-to-implement strategy based on the Spring Expression Language to support class-level & cross-parameter constraints.
Part 1. Implementing your own validation annotations
It is really desirable to write your own validation annotations because you will be able to express different types of constraints in a declarative way and separate validation logic from business logic. I distinguish the following types of constraints as described in the Hibernate Validation documentation:
- Bean constraints: field-level, property-level, class-level and type argument (since Java 8) constraints
- Method constraints: (cross-) parameter and return value constraints
Field-level annotations constrain the value of particular field. Property-level annotations are similar but are annotated on the getter method of a particular field. Class-level annotations constrain the state of an object. This may involve several fields whose values may depend on each other. Type argument annotations ca be used for validation on Java generic types. This allows you to check for null values in a collection, for example. Parameter annotations can be used to check individual parameters of a method. A more advanced mechanism is to validate against cross-parameter constraints which involve the state of a (sub) set of the method's parameters. Return value annotations can be used to validate the returned value of a non-void method. In the following I will elaborate on field-level, class and cross-parameter constraints.
1.1 Field-level constraints
Lets say we want to implement a new annotation which verifies the minimum age of a user to register for a social network. The validation standard defines two date-related annotations @Past and @Future. They allow you to check whether the date of the annotated attribute lies in the past or respectively in the future. In our case we want to check whether the user has an age of at least x years. To achieve this, we have to implement our own annotation first which we call @ValidAge shown in the following code snippet:
import java.lang.annotation.*;
import javax.validation.Constraint;
import javax.validation.Payload;
@Target({ElementType.FIELD, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {AgeValidator.class})
@Documented
public @interface ValidAge {
String message() default "{age.validation.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int min();
}
This annotation is targeted at fields or other annotations and the @Constraint annotation defines by which class the validation will be implemented. Furthermore, we define methods groups() for group-based validation (for further information on this topic, please refer to Chapter 5, Grouping constraints of the Hibernate Validator documentation) and message() / payload() for printing messages when validation fails. These are the standard methods supported by Hibernate Validator. Additionally, we define the min() method to allow the user to define the minimum age to be checked. Next, I will show how to implement age validation class containing the validation logic:
import java.util.Calendar;
import java.util.Date;
import javax.validation.ConstraintValidatorContext;
public class AgeValidator implements ConstraintValidator<ValidAge, Date> {
private ValidAge constraintAnnotation;
public void initialize(ValidAge constraintAnnotation) {
this.constraintAnnotation = constraintAnnotation;
}
public boolean isValid(Date value, ConstraintValidatorContext context) {
if (value == null) {
return true;
} else {
Calendar cal = Calendar.getInstance();
cal.add(Calendar.YEAR, -constraintAnnotation.min());
return !cal.getTime().before(value);
}
}
}
The class responsible for the validation must implement the ConstraintValidator interface with type parameters of the annotation and the type of the annotated field (here java.util.Date). It is also possible to have multiple validation implementation classes to support several types of fields. In this example we could also write a validator for attributes of the new java.time.LocalDateTime class. The isValid() method returns true if the validation is successful. We are now ready to use our new annotation in our domain class called User:
public class User {
@ValidAge(min = 13)
private Date birthday;
}
Spring enables the validation by annotating the bean class with @Validated. You can then annotate method parameters of those beans with @Valid to validate the User object passed as parameter:
@Validated
@Service
@Service
public class UserService {
@Transactional
public User registerUser(@NotNull @Valid User newUser) {
// Implement register logic here
}
}
1.2 Class-level constraints
In some situations field-level annotations are not sufficient, for instance, if you want to validate the state of an object. This often involves more than one attribute and the attribute values may interfere. An example would be the following User class:
public class User {
@NotNull
private String login;
@NotNull
@Email
private String email;
// Getters and setters for login and email not shown here
// Getters and setters for login and email not shown here
}
Let's imagine we don't want to permit null values, neither for login nor for email. However, if we want to check that either login or email must not be null because one of both shall serve as the username (one can be null but not both), we cannot rely on the given set of standard annotations. We need to write our own annotation but this time it must operate on class not on field level.
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { UsernameValidator.class })
@Documented
public @interface ValidUsername {
String message() default "{username.validation.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
public class UsernameValidator implements ConstraintValidator<ValidUsername, User> {
public void initialize(ValidUsername constraintAnnotation) {
}
public boolean isValid(User user, ConstraintValidatorContext context) {
return !(user.getLogin() == null && user.getEmail() == null);
}
}
We can use our new annotation to add it to the User class:
@ValidUsername
@ValidUsername
public class User {
private String login;
@Email
private String email;
// Getters and setters for login and email not shown here
// Getters and setters for login and email not shown here
}
1.3 Cross-parameter constraints
Imagine you have a service method which has two parameters from and until --both of type LocalTimeDate-- and you want to validate that the first parameter from is before until.
@Validated
@Service
@Service
public class UserService {
@ValidDates
@ValidDates
public Collection<User> findUser(LocalDateTime from, LocalDateTime until) {
// Implement findUser logic here
}
}
In order to validate the parameters, you need to define so called cross-parameter constraints. How do you do that? The answer is: just as in the approach above! Start writing a new annotation:
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = { DatesValidator.class })
@Documented
public @interface ValidDates {
String message() default "{dates.validation.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
}
Then implement the DatesValidator class as follows:
import java.time.LocalDateTime;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class DatesValidator implements ConstraintValidator<ValidDates, Object[]> {
public void initialize(ValidDates constraintAnnotation) {
}
public boolean isValid(Object[] value, ConstraintValidatorContext context) {
if (value.length != 2) {
throw new IllegalArgumentException("Only methods with 2 arguments supported!");
}
if (value[0] == null || value[1] == null) {
return true;
}
if (!(value[0] instanceof LocalDateTime) || !(value[1] instanceof LocalDateTime)) {
throw new IllegalArgumentException("Parameters must be of type LocalDateTime!");
}
return ((LocalDateTime) value[0]).isBefore(((LocalDateTime) value[1]));
}
}
Part 2. Implementing powerful validation strategies with the Spring Expression Language
In summary, Hibernate Validation enables you to write your own annotations and to define your own class-level constraints quite easily. However, the code tends to get verbose really quickly and thus it does not scale very well. For instance, you have to write new annotations and validation classes for each specific scenario. Whatever new class-level annotation you defined it will be difficult to reuse for different target classes and scenarios. If we would like to support the annotation of another class besides User in our example, we had to write a new UsernameValidator class which would be specific to that newly supported class. One solution would be to define an interface UsernameProvider defining getLogin() and getEmail(). Our validation class would then use this interface as type parameter:
public class UsernameValidator implements ConstraintValidator<UsernameProvider, User>
All classes you are planning to use our @ValidUsername annotation for must then implement the new UsernameProvider interface.
But what if we want to generalize our username validation strategy into a generic @NotAllAttributesNull constraint which checks whether at least one attribute of the annotated class is not null? We could use Object as the type parameter in the validation class and lookup all getter methods via reflection. However, the code based on reflection tends to get complex and difficult to read. Isn't there any better approach? Yes, for sure! Use Spring Expression Language to implement class-level and cross parameters constraints.
2.1 Class-level constraints
Wouldn't it be nice if we could just define one powerful annotation for all class-level constraints and only one validator class? Yes it is possible and it is not very difficult! In the following I will show you how to accomplish that.
Let's start again with a new annotation called @ValidateClassExpression:
Let's start again with a new annotation called @ValidateClassExpression:
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = {SpELClassValidator.class})
@Documented
public @interface ValidateClassExpression {
String message() default "{expression.validation.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String value();
}
As you can see I've specified an extra attribute called value which allows us to define our concrete (Spring expression language, SpEL) expression. Now have a look on how simple it is to write the validator class based on SpEL:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class SpELClassValidator implements ConstraintValidator<ValidateClassExpression,
Object> {
private ValidateClassExpression annotation;
private ExpressionParser parser = new SpelExpressionParser();
public void initialize(ValidateClassExpression constraintAnnotation) {
annotation = constraintAnnotation;
parser.parseExpression(constraintAnnotation.value());
}
public boolean isValid(Object value, ConstraintValidatorContext context) {
StandardEvaluationContext spelContext = new StandardEvaluationContext(value);
return (Boolean) parser.parseExpression(annotation.value()).getValue(spelContext);
}
}
This class is able to evaluate the SpEL expression given by the expression attribute of your annotation and returns the result as the result for your validation! Please make yourself a bit familiar with SpEL to understand how powerful and flexible the application of this approach is (please refer to the Spring docs: http://docs.spring.io/spring/docs/current/spring-framework-reference/html/expressions.html)
The starting point for your evaluation SpEL's #this variable pointing to the current evaluation object which is the object to be validated in our case. You can access all properties and invoke all methods of the object using SpEL.
Lets come back to our User class and apply the new validator for checking whether login and email are null.
@ValidateClassExpression(value = "!(#this.login == null && #this.email == null)", message = "Login or email must be defined.")
public class User {
private String login;
@Email
private String email;
}
Great :-). And imagine user has another attribute which is collection of cars. How could we check that the user has at least one car? It is so easy:
@ValidateClassExpression(value = "!#this.cars.isEmpty()", message = "User must have at least one car.")
public class User {
private List<Car> cars = new ArrayList<>();
// Getters and setters for login and email not shown here
// Getters and setters for login and email not shown here
}
We could also combine several expressions by a simple conjunction of both expressions but the downside of this approach is that you could only use one validation message instead of a specific message for each constraint.
@ValidateClassExpression(value = "!(#this.login == null && #this.email == null) && !#this.cars.isEmpty()", message = "Validation failed")
No problem! Since Java 8 it is possible to make annotations repeatable. Thus, we can add the following annotation to the @ValidateClassExpression annotation:
@Repeatable(ValidateClassExpressions.class)
Furthermore, we have to write a new container annotation:
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidateClassExpressions {
ValidateClassExpression[] value();
}
This enables us to put multiple ValidateClassExpression annotations to the User class with individual error messages:
@ValidateClassExpression(value = "!(#this.login == null && #this.email == null)", message = "Login or email must be defined.")
@ValidateClassExpression(value = "!#this.cars.isEmpty()", message = "User must have at least one car.")
public class User {
private String login;
@Email
private String email;
private List<Car> cars = new ArrayList<>();
// Getters and setters for login, email and cars not shown here
private List<Car> cars = new ArrayList<>();
// Getters and setters for login, email and cars not shown here
}
Now you have a great toolset to write really flexible and powerful constraints. Last but not least let's have a look at how to apply this concept to cross parameter constraints as well.
Now you have a great toolset to write really flexible and powerful constraints. Last but not least let's have a look at how to apply this concept to cross parameter constraints as well.
2.2 Cross-parameter constraints
For the validation of cross-parameters we first define a new annotation:
@Constraint(validatedBy = SpELParameterValidator.class)
@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR, ElementType.ANNOTATION_TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidateParametersExpression {
String message() default "{parameters.validation.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
String value();
}
Then, we implement the validation class as follows:
@SupportedValidationTarget(ValidationTarget.PARAMETERS)
public class SpELParameterValidator implements ConstraintValidator< ValidateParametersExpression,
Object[]> {
Object[]> {
private ValidateParametersExpression validParameters;
private ExpressionParser parser = new SpelExpressionParser();
public void initialize(ValidateParametersExpression
constraintAnnotation) {
constraintAnnotation) {
this.validParameters = constraintAnnotation;
parser.parseExpression(constraintAnnotation.value());
}
public boolean isValid(Object[] values, ConstraintValidatorContext context) {
StandardEvaluationContext spelContext = new StandardEvaluationContext(values);
Map<String, Object> spelVars = IntStream.range(0,
values.length).boxed().collect(Collectors.toMap(
values.length).boxed().collect(Collectors.toMap(
i -> "arg" + i,
i -> values[i]
));
spelContext.setVariables(spelVars);
Boolean evaluationValue = (Boolean)
parser.parseExpression(validParameters.value()).getValue(spelContext);
parser.parseExpression(validParameters.value()).getValue(spelContext);
return evaluationValue;
}
}
@Validated
@Service
@Service
public class UserService {
@ValidateParametersExpression("#arg0.isBefore(#arg1)")
@ValidateParametersExpression("#arg0.isBefore(#arg1)")
public Collection<User> findUser(LocalTimeDate from, LocalTimeDate until) {
// Implement findUser logic here
}
}
How powerful and simple! In my opinion simplicity and productivity for writing constraints is inherently important because otherwise you might skip certain validation constraint because of the high implementation effort and this could lead to illegal states in the future.
I hope you found this article helpful and it inspired you to combine the Spring Expression language with Hibernate Validator in the future. The two annotations that we've implemented using SpEL @ValidClassExpression and @ValidParametersExpression can be used for all kinds of use cases related to class-level or cross-parameter validation. This allows you to apply validation to all your classes and methods which will definitely increase your code quality in the long run and helps you to separate your validation from your business code.
I am looking forward to any comments, improvements or extensions to the presented approach. Thanks for reading. You can checkout the code examples presented in this article from my Github repository: https://github.com/Javatar81/code-examples/tree/master/spelvalidation