Different validation groups for method parameters and return value

M Bush :

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?

M Bush :

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();
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=17376&siteId=1