I would like to use different validation groups for a method's parameters and its return value. I am trying to Spring's @Validated annotation to achieve this. For example:
public interface UserOperations {
@Validated({ResponseGroup.class})
@Valid
User createUser(@Validated({RequestGroup.class}) @Valid User user);
}
It appears that return value is in fact getting validated against ResponseGroup
, however the method argument user
is getting validated against both ResponseGroup
and RequestGroup
. I see why this is happening when I look at: org.springframework.validation.beanvalidation.MethodValidationInterceptor#invoke
Is there an elegant way to apply one validation group to the method argument and different validation group to the return value?
The problem is that the method MethodValidationInterceptor.invoke
looks for validation groups defined in an @Validated
annotation on each method, or on the class, but not on individual parameters. The validation groups that are specified will be applied to the parameters as well as the return value.
In order to have different validation groups get applied to parameters and return values, I created two annotations which will be used to specify validation groups for return values and validation groups for arguments respectively:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidateReturnValue {
Class<?>[] value();
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ValidateArguments {
Class<?>[] value();
}
Now one set of validation groups can be specified to validate the return values and a different set of validation groups can be specified for all method arguments:
@Validated
public interface MyClassWithMethodsToValidate {
@ValidateReturnValue({FooResponseGroup.class})
@ValidateArguments({FooRequestGroup.class})
FooResource createFoo(Foo foo);
}
In order for these annotations to be processed, I created a class that extends MethodValidationInterceptor
and overrides its invoke
method (much of the code is the same as the overridden method)
import org.aopalliance.intercept.MethodInvocation;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.SmartFactoryBean;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ClassUtils;
import org.springframework.validation.beanvalidation.MethodValidationInterceptor;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.Validation;
import javax.validation.Validator;
import java.lang.reflect.Method;
import java.util.Set;
class MyMethodValidationInterceptor extends MethodValidationInterceptor {
private final Validator validator;
public MyMethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory().getValidator());
}
public MyMethodValidationInterceptor(Validator validator) {
super(validator);
this.validator = validator;
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
Method methodToValidate = invocation.getMethod();
Class<?>[] parameterGroups = getArgumentsGroups(methodToValidate);
if(parameterGroups == null){
parameterGroups = super.determineValidationGroups(invocation);
}
Set<ConstraintViolation<Object>> violations;
try {
violations = validator.forExecutables().validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), parameterGroups);
}
catch (IllegalArgumentException ex) {
methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
violations = validator.forExecutables().validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), parameterGroups);
}
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
Object returnValue = invocation.proceed();
Class<?>[] returnValueGroups = getReturnValueGroups(methodToValidate);
if(returnValueGroups == null){
returnValueGroups = super.determineValidationGroups(invocation);
}
violations = validator.forExecutables().validateReturnValue(invocation.getThis(), methodToValidate, returnValue, returnValueGroups);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
return returnValue;
}
private Class<?>[] getReturnValueGroups(Method methodToValidate){
ValidateReturnValue annotation = AnnotationUtils.findAnnotation(methodToValidate, ValidateReturnValue.class);
if(annotation != null){
return annotation.value();
}else {
return null;
}
}
private Class<?>[] getArgumentsGroups(Method methodToValidate){
ValidateArguments annotation = AnnotationUtils.findAnnotation(methodToValidate, ValidateArguments.class);
if(annotation != null){
return annotation.value();
}else {
return null;
}
}
private boolean isFactoryBeanMetadataMethod(Method method) {
Class<?> clazz = method.getDeclaringClass();
if (clazz.isInterface()) {
return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
!method.getName().equals("getObject"));
}
Class<?> factoryBeanType = null;
if (SmartFactoryBean.class.isAssignableFrom(clazz)) {
factoryBeanType = SmartFactoryBean.class;
}
else if (FactoryBean.class.isAssignableFrom(clazz)) {
factoryBeanType = FactoryBean.class;
}
return (factoryBeanType != null && !method.getName().equals("getObject") &&
ClassUtils.hasMethod(factoryBeanType, method));
}
}
Then I created a class that extends MethodValidationPostProcessor
and overrides its createMethodValidationAdvice
method to use the new MyMethodValidationInterceptor
class:
class MyMethodValidationPostProcessor extends MethodValidationPostProcessor {
@Override
protected Advice createMethodValidationAdvice(Validator validator) {
return (validator != null ? new MyMethodValidationInterceptor(validator) : new MyMethodValidationInterceptor());
}
}
Finally, configure a bean of MyMethodValidationPostProcessor
@Configuration
public class MethodValidationConfig {
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MyMethodValidationPostProcessor();
}
}