Spring 方法级别数据校验:@Validated + MethodValidationPostProcessor 优雅的完成数据校验动作

效果示例

@Validated(Default.class)
public interface HelloService {
    Object hello(@NotNull @Min(10) Integer id, @NotNull String name);
}

// 实现类如下
@Slf4j
@Service
public class HelloServiceImpl implements HelloService {
    @Override
    public Object hello(Integer id, String name) {
        return null;
    }
}

向容器里注册一个处理器:

@Configuration
public class RootConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        return new MethodValidationPostProcessor();
    }
}

测试:

@Slf4j
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RootConfig.class})
public class TestSpringBean {
    @Autowired
    private HelloService helloService;

    @Test
    public void test1() {
        System.out.println(helloService.getClass());
        helloService.hello(1, null);
    }
}

结果如图:

若需要校验方法返回值,改写如下:

    @NotNull
    Object hello(Integer id);

	// 此种写法效果同上
    //@NotNull Object hello(Integer id);

运行:

javax.validation.ConstraintViolationException: hello.: 不能为null
...

校验完成。就这样借助Spring+JSR相关约束注解,就非常简单明了,语义清晰的优雅的完成了方法级别(入参校验、返回值校验)的校验。
校验不通过的错误信息,再来个全局统一的异常处理,就能让整个工程都能尽显完美之势。(错误消息可以从异常ConstraintViolationException的getConstraintViolations()方法里获得的~)
 

MethodValidationPostProcessor

它是Spring提供的来实现基于方法MethodJSR校验的核心处理器~它能让约束作用在方法入参、返回值上,如:

public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)

官方说明:方法里写有JSR校验注解要想其生效的话,要求类型级别上必须使用@Validated标注(还能指定验证的Group)

另外提示一点:这个处理器同处理@Async的处理器AsyncAnnotationBeanPostProcessor非常相似,都是继承自AbstractBeanFactoryAwareAdvisingPostProcessor

// @since 3.1
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor implements InitializingBean {
	// 备注:此处你标注@Valid是无用的~~~Spring可不提供识别
	// 当然你也可以自定义注解(下面提供了set方法~~~)
	// 但是注意:若自定义注解的话,此注解只决定了是否要代理,并不能指定分组哦  so,没啥事别给自己找麻烦吧
	private Class validatedAnnotationType = Validated.class;
	// 这个是javax.validation.Validator
	@Nullable
	private Validator validator;

	// 可以自定义生效的注解
	public void setValidatedAnnotationType(Class validatedAnnotationType) {
		Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
		this.validatedAnnotationType = validatedAnnotationType;
	}

	// 这个方法注意了:你可以自己传入一个Validator,并且可以是定制化的LocalValidatorFactoryBean哦~(推荐)
	public void setValidator(Validator validator) {
		// 建议传入LocalValidatorFactoryBean功能强大,从它里面生成一个验证器出来靠谱
		if (validator instanceof LocalValidatorFactoryBean) {
			this.validator = ((LocalValidatorFactoryBean) validator).getValidator();
		} else if (validator instanceof SpringValidatorAdapter) {
			this.validator = validator.unwrap(Validator.class);
		} else {
			this.validator = validator;
		}
	}
	// 当然,你也可以简单粗暴的直接提供一个ValidatorFactory即可~
	public void setValidatorFactory(ValidatorFactory validatorFactory) {
		this.validator = validatorFactory.getValidator();
	}


	// 毫无疑问,Pointcut使用AnnotationMatchingPointcut,并且支持内部类哦~
	// 说明@Aysnc使用的也是AnnotationMatchingPointcut,只不过因为它支持标注在类上和方法上,所以最终是组合的ComposablePointcut
	
	// 至于Advice通知,此处一样的是个`MethodValidationInterceptor`~~~~
	@Override
	public void afterPropertiesSet() {
		Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
		this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
	}
	
	// 这个advice就是给@Validation的类进行增强的~  说明:子类可以覆盖哦~
	// @since 4.2
	protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
		return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
	}
}

它是个普通的BeanPostProcessor,为Bean创建的代理的时机是postProcessAfterInitialization(),也就是在Bean完成初始化后有必要的话用一个代理对象返回进而交给Spring容器管理~(同@Aysnc)
容易想到,关于校验方面的逻辑不在于它,而在于切面的通知:MethodValidationInterceptor
 

MethodValidationInterceptor

它是AOP联盟类型的通知,此处专门用于处理方法级别的数据校验

// @since 3.1  因为它校验Method  所以它使用的是javax.validation.executable.ExecutableValidator
public class MethodValidationInterceptor implements MethodInterceptor {

	// javax.validation.Validator
	private final Validator validator;

	// 如果没有指定校验器,那使用的就是默认的校验器
	public MethodValidationInterceptor() {
		this(Validation.buildDefaultValidatorFactory());
	}
	public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
		this(validatorFactory.getValidator());
	}
	public MethodValidationInterceptor(Validator validator) {
		this.validator = validator;
	}


	@Override
	@SuppressWarnings("unchecked")
	public Object invoke(MethodInvocation invocation) throws Throwable {
		// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
		// 如果是FactoryBean.getObject() 方法  就不要去校验了~
		if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
			return invocation.proceed();
		}

		Class[] groups = determineValidationGroups(invocation);

		// Standard Bean Validation 1.1 API  ExecutableValidator是1.1提供的
		ExecutableValidator execVal = this.validator.forExecutables();
		Method methodToValidate = invocation.getMethod();
		Set> result; // 错误消息result  若存在最终都会ConstraintViolationException异常形式抛出

		try {
			// 先校验方法入参
			result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
		} catch (IllegalArgumentException ex) {
			// 此处回退了异步:找到bridged method方法再来一次
			methodToValidate = BridgeMethodResolver.findBridgedMethod(ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
			result = execVal.validateParameters(invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
		}
		if (!result.isEmpty()) { // 有错误就抛异常抛出去
			throw new ConstraintViolationException(result);
		}
		// 执行目标方法  拿到返回值后  再去校验这个返回值
		Object returnValue = invocation.proceed();
		result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
		if (!result.isEmpty()) {
			throw new ConstraintViolationException(result);
		}

		return returnValue;
	}


	// 找到这个方法上面是否有标注@Validated注解  从里面拿到分组信息
	// 备注:虽然代理只能标注在类上,但是分组可以标注在类上和方法上哦~~~~ 
	protected Class[] determineValidationGroups(MethodInvocation invocation) {
		Validated validatedAnn = AnnotationUtils.findAnnotation(invocation.getMethod(), Validated.class);
		if (validatedAnn == null) {
			validatedAnn = AnnotationUtils.findAnnotation(invocation.getThis().getClass(), Validated.class);
		}
		return (validatedAnn != null ? validatedAnn.value() : new Class[0]);
	}
}

这个Advice的实现,简单到不能再简单了,稍微有点基础的应该都能很容易看懂吧(据我不完全估计这个应该是最简单的)。

你可能感兴趣的:(spring)