SpringMVC可以使用Validation bean验证框架去校验Controller层请求参数是否符合要求。但是如何在Service层或者dao层去统一的校验方法参数或返回值呢?
注解版参数校验
在书写业务逻辑(service/dao)时,有时会书写大量的参数或返回值校验(例如非空判断),一般有如下两种方式:
- 采用if...else标签进行校验。即编程式的业务参数校验,将业务逻辑与参数判断整合在一起。
- 采用注解的方式进行校验,即声明式的业务参数校验。使得业务代码和数据校验解耦。
通过注解来校验service或dao层的方法参数,便是Spring AOP的一种体现。
简易使用流程
环境:SpringBoot2.x,JDK8
import org.springframework.validation.annotation.Validated;
import javax.validation.constraints.NotNull;
import javax.validation.groups.Default;
@Validated(Default.class)
public interface IAccount {
@NotNull //返回值不能为null
String say(@NotNull Integer id, @NotNull String desc);
}
@Service
public class AccountImpl implements IAccount {
public String say(Integer id, String desc) {
System.out.println(String.format("The id is [%s] ,The desc is [%s]", id, desc));
return null;
}
}
@RestController
public class AopController {
@Autowired
private IAccount account;
@RequestMapping("/test")
public void test1(){
account.say(1,null);
}
}
返回结果:
2019-11-18 13:45:12,439 ERROR [33568] [http-nio-8082-exec-1] [] (DirectJDKLog.java:175): Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is javax.validation.ConstraintViolationException: say.desc: 不能为null] with root cause
javax.validation.ConstraintViolationException: say.desc: 不能为null
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
at com.tellme.Impl.AccountImpl$$EnhancerBySpringCGLIB$$c934e438.say()
at com.tellme.controller.AopController.test1(AopController.java:32)
若是校验不通过,可以借助全局异常统一处理。可以在ConstraintViolationException
的getConstraintViolations
方法获取到错误信息。
企业使用流程
上面的例子,我们已经了解到可以通过注解的形式(Spring+AOP)来校验参数、方法值。但是我们应该了解其使用的一些细节。
- 若service无接口,那么MethodValidation是否生效。
因为是在Bean初始化时,在后置处理器上解析MethodValidationPostProcessor
标签。生成代理对象,所以若service无接口时,会生成cglib代理,依旧可以在方法级别上生效。
- 校验注解(@NotNull)可以写在实现类上吗?
public interface IAccount {
String say( Integer id, String desc);
}
@Validated(Default.class)
@Service
public class AccountImpl implements IAccount {
public @NotNull String say(@NotNull Integer id, String desc) {
System.out.println(String.format("The id is [%s] ,The desc is [%s]", id, desc));
return null;
}
}
异常原因:重写另一个方法的方法不能重新定义参数的约束规则。
javax.validation.ConstraintDeclarationException: HV000151: A method overriding another method must not redefine the parameter constraint configuration, but method AccountImpl
在上述异常中,我们可以把握住两条基本信息:
(1)重写方法不能重新定义参数的约束规则;
(2)重写方法可以重写定义返回值的约束规则!并且@Validated注解可以定义在子类上。
public interface IAccount {
String say(@NotNull Integer id, @NotNull String desc);
}
@Validated(Default.class)
@Service
public class AccountImpl implements IAccount {
public @NotNull String say(Integer id, String desc) {
System.out.println(String.format("The id is [%s] ,The desc is [%s]", id, desc));
return null;
}
}
返回结果:返回值不能为null(可以看到子类的返回值注解生效)。
javax.validation.ConstraintViolationException: say.: 不能为null
- 对参数对象的校验
public interface IAccount {
String sayPerson(@NotNull @Valid Person person);
}
@Validated(Default.class)
@Service
public class AccountImpl implements IAccount {
@Override
public String sayPerson(Person person) {
return JSON.toJSONString(person);
}
}
@RestController
public class AopController {
@Autowired
private IAccount account;
@RequestMapping("/test2")
public void test2() {
account.sayPerson(null);
}
}
返回结果:若是对象为null,则返回异常,证明参数注解生效。
javax.validation.ConstraintViolationException: sayPerson.person: 不能为null
- 循环依赖问题
MethodValidation原理显然是AOP。和事务一样:若本类的A方法调用本类带有校验注解的B方法,那么B方法的方法校验注解不会生效。
public interface IAccount {
String say(@NotNull Integer id, @NotNull String desc);
String sayTo();
}
@Validated(Default.class)
@Service
public class AccountImpl implements IAccount {
public @NotNull String say(Integer id, String desc) {
System.out.println(String.format("The id is [%s] ,The desc is [%s]", id, desc));
return null;
}
@Override
public String sayTo() {
return say(1, "内部调用");
}
}
@RestController
public class AopController {
@Autowired
private IAccount account;
@RequestMapping("/test4")
public void test4(){
account.sayTo();
}
}
执行方法,say()方法虽然返回null,但是并未抛出异常。即校验注解未生效!
推荐方式:
启动类加上
@EnableAspectJAutoProxy(exposeProxy = true)
开启事务的AOP通知。配置类上将
methodValidationPostProcessor
和@Async
都可以使用AopContext。
@Component
public class ModifyExposeProxy2BeanFactoryPostProcessor implements BeanFactoryPostProcessor {
/**
* 修改后置处理器的BeanDefinition对象。
* @param beanFactory
* @throws BeansException
*/
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
//修改service层注解的BeanPostProcessor
BeanDefinition methodValidationPostProcessor = beanFactory.getBeanDefinition("methodValidationPostProcessor");
methodValidationPostProcessor.getPropertyValues().add("exposeProxy",true);
//修改@Async的BeanPostProcessor
BeanDefinition beanDefinition = beanFactory.getBeanDefinition(TaskManagementConfigUtils.ASYNC_ANNOTATION_PROCESSOR_BEAN_NAME);
beanDefinition.getPropertyValues().add("exposeProxy", true);
}
}
注意点
- 请不要在异步线程里使用AopContext.currentProxy();
- AopContext.currentProxy()不能使用在非代理对象所在方法体内;
@Validated(Default.class)
@Service
public class AccountImpl implements IAccount {
public @NotNull String say(Integer id, String desc) {
System.out.println(String.format("The id is [%s] ,The desc is [%s]", id, desc));
return null;
}
@Override
public String sayTo() {
//获取当前对象的代理对象
IAccount account = IAccount.class.cast(AopContext.currentProxy());
return account .say(1, "内部调用");
}
}
核心原理
它是Spring提供的基于Method的JSR校验的核心处理器。它可以让约束作用在入参和返回值上。
注意:若是方法级别注解校验生效的话,那么必须在类级别上使用@Validated标注。
若是想了解BeanPostProcessor Spring Bean的后置处理器,请点击...
而MethodValidationPostProcessor
正是BeanPostProcessor
的一种实现,它委托JSR-303提供程序,用于带注释的方法执行方法级别的验证。
它作为一种普通的BeanPostProcessor
,其作用便是在Bean初始化时解析标签,增加数据校验的功能,最终生成一个代理对象。
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
//解析带有@validated标签的类对象
private Class extends Annotation> validatedAnnotationType = Validated.class;
@Nullable
private Validator validator;
//设置自定义-生效的标签,必须是Annotation的子类
public void setValidatedAnnotationType(Class extends Annotation> validatedAnnotationType) {
Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
this.validatedAnnotationType = validatedAnnotationType;
}
//可以自己传入一个Validator。推荐使用定制化的LocalValidatorFactoryBean
public void setValidator(Validator validator) {
// Unwrap to the native Validator with forExecutables support
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。
//生成的advisor,而advice则是MethodValidationInterceptor。
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
//而advice则是给@Validation类进行增强的。
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
在MethodValidationPostProcessor
源码中,可以看到,其主要作用是定位到切点(pointcut),并获取到advisor(advice和pointcut)。所以,拦截的主要逻辑在advice中,即MethodValidationInterceptor
中。
public class MethodValidationInterceptor implements MethodInterceptor {
//比较器
private final Validator validator;
//若没有指定比较器,则使用默认的比较器
public MethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory());
}
//根据上送的ValidatorFactory获取比较器
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 execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
//若是生成错误消息result,最终存放在该set中。
Set> result;
try {
//校验方法入参
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// 此处回退一步,找到桥接方法后再来一次
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);
//若是Set集合不为空,则将异常抛出
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]);
}
}
推荐阅读
1. Spring方法级别数据校验:@Validated + MethodValidationPostProcessor优雅的完成数据校验动作
2. 从@Async案例找到Spring框架的bug:exposeProxy=true不生效原因大剖析+最佳解决方案【享学Spring】