Spring custom validator from interface
When we use layers separation it sometimes become problematic to use custom validator annotation, as to execute it we need service layer not available in data layer. After some digging around I have found a solution, to set validatedBy annotation paramter to interface class instead of implementation class and handle that in the service layer with customized validator factory code.
First we need a class resolving a validator from an interface (if thats interface implementation is available as spring context bean, so annotated with eg. @Component)
:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
/**
* Similar to SpringConstraintValidatorFactory, but look also for interface implementation to avoid
* cross dependency between layers because of @Constraint validatedBy
*/
public class InterfaceResolvingSpringConstraintValidatorFactory implements ConstraintValidatorFactory {
private final Logger logger = LoggerFactory.getLogger(InterfaceResolvingSpringConstraintValidatorFactory.class);
private final AutowireCapableBeanFactory beanFactory;
public InterfaceResolvingSpringConstraintValidatorFactory(AutowireCapableBeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
@Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
T bean = null;
try {
logger.debug("Trying to find a validator bean implementing class " + key.getSimpleName());
bean = beanFactory.getBean(key);
} catch (BeansException exc) {
logger.info("Failed to find a bean of class " + key.getSimpleName());
}
if (bean == null) {
try {
logger.debug("Creating a new validator bean of class " + key.getSimpleName());
bean = beanFactory.createBean(key);
} catch (BeansException exc) {
logger.info("Failed to create a validator of class " + key.getSimpleName());
}
}
if (bean == null) {
logger.warn("Failed to get validator of class " + key.getSimpleName());
//optionally throw some exception here instead
}
return bean;
}
@Override
public void releaseInstance(ConstraintValidator<?, ?> constraintValidator) {
beanFactory.destroyBean(constraintValidator);
}
}
Seond we use that class in custom LocalValidatorFactoryBean:
import javax.validation.Configuration;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Component
public class InterfaceResolvingLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
super.setApplicationContext(applicationContext);
this.applicationContext = applicationContext;
}
@Override
protected void postProcessConfiguration(Configuration<?> configuration) {
InterfaceResolvingSpringConstraintValidatorFactory constraintFactory = new InterfaceResolvingSpringConstraintValidatorFactory(
applicationContext.getAutowireCapableBeanFactory());
configuration.constraintValidatorFactory(constraintFactory);
}
}
Last we need to prepare configuration class, returning MethodValidationPostProcessor
with our custom factory set:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
@Configuration
public class ValidationConfig {
@Autowired
private InterfaceResolvingLocalValidatorFactoryBean validatorFactoryBean;
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
processor.setValidator(validatorFactoryBean);
return processor;
}
}
Now @Contraint
can use validatedBy = ValidationInterface.class
instead of validatedBy = ValidationImplementation.class
.
Please note that final solution can be also achieved in some other ways, for example with the use of Hibernate ServiceLoader described in a post on in.relation.to.