Adding custom ConstraintValidator for @Future and LocalDate to a Spring Boot project
I'd agree with Miloš that using the META-INF/validation.xml
is probably the cleanest and easiest way, but if you do really want to set it up in a Spring @Confguration
class then it is possible and here's one way you can do it.
The beauty of Spring Boot is that does a lot of configuration on your behalf so you don't have to worry about it. However, this can also cause problems when you want to specifically configure something yourself and it's not terribly obvious how to do it.
So yesterday I set about trying to add a CustomerValidator
for @Past
and LocalDate
using the ConstraintDefinitionContributor
mechanism that Hardy suggests (and is referred to in the Hibernate documentation).
The simple bit was to write the implementing class to do the validation, which for my highly specific purposes consisted of:
public class PastValidator implements ConstraintValidator<Past, LocalDate> {
@Override
public void initialize(Past constraintAnnotation) {}
@Override
public boolean isValid(LocalDate value, ConstraintValidatorContext context) {
return null != value && value.isBefore(LocalDate.now());
}
}
Then I got lazy and just instantiated a @Bean
in my configuration, just on the off-chance that one of Spring's auto-configuration classes would just pick it up and wire it into the Hibernate validator. This was quite a long shot, given the available documentation (or lack thereof) and what Hardy and others had said, and it didn't pay off.
So I fired up a debugger and worked backwards from the exception being thrown in org.hibernate.validator.internal.engine.constraintvalidation.ConstraintTree
which was telling me that it couldn't find a validator for @Past
and LocalDate
.
Looking at the type hierarchy of the ConstraintValidatorFactory
I discovered that there were two implementing classes in my Spring MVC application: SpringConstraintValidatorFactory
and SpringWebConstraintValidatorFactory
which both just try and get a bean from the context of the correct class. This told me that I do have to have my validator registered with Spring's BeanFactory
, however when I stuck a breakpoint on this but it didn't get hit for my PastValidator
, which meant that Hibernate wasn't aware that it should be even requesting this class.
This made sense: there wasn't any ConstraintDefinitionContributor
anywhere to do tell Hibernate it needed to ask Spring for an instance of the PastValidator. The example in the documentation at
http://docs.jboss.org/hibernate/validator/5.2/reference/en-US/html_single/#section-constraint-definition-contributor
suggests that I'd need access to a HibernateValidatorConfiguration
so I just needed to find where Spring was doing its configuring.
After a little bit of digging I found that it was all happening in Spring's LocalValidatorFactoryBean
class, specifically in its afterPropertiesSet()
method. From its javadoc:
/*
* This is the central class for {@code javax.validation} (JSR-303) setup in a Spring
* application context: It bootstraps a {@code javax.validation.ValidationFactory} and
* exposes it through the Spring {@link org.springframework.validation.Validator} interface
* as well as through the JSR-303 {@link javax.validation.Validator} interface and the
* {@link javax.validation.ValidatorFactory} interface itself.
*/
Basically, if you don't set up and configure your own Validator then this is where Spring tries to do it for you, and in true Spring style it provides a handy extension method so that you can let it do its configuration and then add your own into the mix.
So my solution was to just extend the LocalValidatorFactoryBean
so that I'd be able to register my own ConstraintDefinitionContributor
instances:
import java.util.ArrayList;
import java.util.List;
import javax.validation.Configuration;
import org.hibernate.validator.internal.engine.ConfigurationImpl;
import org.hibernate.validator.spi.constraintdefinition.ConstraintDefinitionContributor;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
public class ConstraintContributingValidatorFactoryBean extends LocalValidatorFactoryBean {
private List<ConstraintDefinitionContributor> contributors = new ArrayList<>();
public void addConstraintDefinitionContributor(ConstraintDefinitionContributor contributor) {
contributors.add(contributor);
}
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
if(configuration instanceof ConfigurationImpl) {
ConfigurationImpl config = ConfigurationImpl.class.cast(configuration);
for(ConstraintDefinitionContributor contributor : contributors)
config.addConstraintDefinitionContributor(contributor);
}
}
}
and then instantiate and configure this in my Spring config:
@Bean
public ConstraintContributingValidatorFactoryBean validatorFactory() {
ConstraintContributingValidatorFactoryBean validatorFactory = new ConstraintContributingValidatorFactoryBean();
validatorFactory.addConstraintDefinitionContributor(new ConstraintDefinitionContributor() {
@Override
public void collectConstraintDefinitions(ConstraintDefinitionBuilder builder) {
builder.constraint( Past.class )
.includeExistingValidators( true )
.validatedBy( PastValidator.class );
}
});
return validatorFactory;
}
and for completeness, here's also where I'd instantiated by PastValidator
bean:
@Bean
public PastValidator pastValidator() {
return new PastValidator();
}
Other springy-thingies
I noticed in my debugging that because I've got quite a large Spring MVC application, I was seeing two instances of SpringConstraintValidatorFactory
and one of SpringWebConstraintValidatorFactory
. I found the latter
was never used during validation so I just ignored it for the time being.
Spring also has a mechanism for deciding which implementation of ValidatorFactory
to use, so it's possible for it to not use your ConstraintContributingValidatorFactoryBean
and instead use
something else (sorry, I found the class in which it did this yesterday but couldn't find it again today although I only spent about 2 minutes looking). If you're using Spring MVC in any kind of non-trivial way
then chances are you've already had to write your own configuration class such as this one which implements WebMvcConfigurer
where you can explicitly wire in your Validator
bean:
public static class MvcConfigurer implements WebMvcConfigurer {
@Autowired
private ConstraintContributingValidatorFactoryBean validatorFactory;
@Override
public Validator getValidator() {
return validatorFactory;
}
// ...
// <snip>lots of other overridden methods</snip>
// ...
}
This is wrong
As has been pointed out, you should be wary of applying a @Past
validation to a LocalDate
because there's no time zone information. However, if you're using LocalDate
because everything will just run in the same time zone, or you deliberately want to ignore time zones, or you just don't care, then this is fine for you.
You'll need to add your own validator to the META-INF/validation.xml
file, like so:
<constraint-mappings
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://jboss.org/xml/ns/javax/validation/mapping validation-mapping-1.1.xsd"
xmlns="http://jboss.org/xml/ns/javax/validation/mapping" version="1.1">
<constraint-definition annotation="javax.validation.constraints.Future">
<validated-by include-existing-validators="true">
<value>package.to.LocalDateFutureValidator</value>
</validated-by>
</constraint-definition>
</constraint-mappings>
For more details, refer to the official documentation.
In case someone has a problem with the validation.xml
approach and is getting Cannot find the declaration of element constraint-mappings
error, as I did, I had to make the following modifications. Hope this will save somebody the time I spent to figure this out.
META-INF/validation.xml
:
<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">
<constraint-mapping>META-INF/validation/past.xml</constraint-mapping>
</validation-config>
META-INF/validation/past.xml
:
<constraint-mappings
xmlns="http://jboss.org/xml/ns/javax/validation/mapping"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://jboss.org/xml/ns/javax/validation/mapping
validation-mapping-1.1.xsd"
version="1.1">
<constraint-definition annotation="javax.validation.constraints.Past">
<validated-by include-existing-validators="false">
<value>your.package.PastConstraintValidator</value>
</validated-by>
</constraint-definition>
</constraint-mappings>