0 版本信息
Spring Boot:2.1.7.RELEASE
Spring: 5.1.9.RELEASE
validation-api: 2.0.1.Final
hibernate-validator: 6.0.17.Final
1 问题的提出
一般我们在写SpringMVC接口时,可能会使用@RequestParam来解释GET请求中的参数,比如我们想要根据userId获取用户详细信息:
@GetMapping("/user")
@ResponseBody
public Result<Map<String, Object>> test(@RequestParam String userId) {
Map<String, Object> userDetail = new HashMap<String, Object>();
userDetail.put("userId", userId);
userDetail.put("userName", "test user");
return new Result<Map<String, Object>>(userDetail);
}
上面例子存在的问题:
1. 只能保证userId在URL中是存在的,但不能保证userId是否有值
2. 不能限制userId的输入长度,假设userId的长度
2 方法级别的验证
要点:
1. 添加MethodValidationPostProcessor
2. 在需要进行方法验证的类添加@Validated注解
3. 给方法参数添加需要的验证注解
4. 需要捕获Controller中方法抛出的ConstraintViolationException
2.1 添加MethodValidationPostProcessor
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
@SpringBootApplication
public class MethodLevelValidationApplication {
public static void main(String[] args) {
SpringApplication.run(MethodLevelValidationApplication.class, args);
}
// 要点1:新增MethodValidationPostProcessor这个Bean
@Bean
public MethodValidationPostProcessor methodValidationPostProcessor() {
return new MethodValidationPostProcessor();
}
}
2.2 给Controller中的方法添加方法级别的数据验证
package com.example.demo;
import java.util.HashMap;
import java.util.Map;
import javax.validation.constraints.NotBlank;
import org.hibernate.validator.constraints.Length;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Validated // 要点2:在需要进行方法验证的类添加@Validated注解
public class TestController {
@GetMapping("/user")
@ResponseBody
public Result<Map<String, Object>> test(
// 要点3:给方法参数添加需要的验证注解
@NotBlank(message = "userId must not be blank")
@Length(min = 6, max = 6, message = "the length of userId must be {min} ")
@RequestParam String userId) {
return new Result.Builder<Map<String, Object>>()
.data(getUserDetailById(userId))
.build();
}
private Map<String, Object> getUserDetailById(String userId) {
Map<String, Object> userDetail = new HashMap<String, Object>();
userDetail.put("userId", userId);
userDetail.put("userName", "test user");
return userDetail;
}
}
2.3 ControllerAdvice处理数据验证异常
package com.example.demo;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// 要点4:需要捕获Controller中方法抛出的ConstraintViolationException
@RestControllerAdvice
public class GlobalExceptionHander {
@ExceptionHandler(value = RuntimeException.class)
public String validationHander(RuntimeException e) {
return e.toString();
}
@ExceptionHandler(value = ConstraintViolationException.class)
@ResponseStatus(value = HttpStatus.BAD_REQUEST)
public Result<Void> validationHandler(ConstraintViolationException e) {
StringBuilder message = new StringBuilder("Invalid Request Parameters: ");
for(ConstraintViolation<?> constraintViolation : e.getConstraintViolations()) {
message.append(constraintViolation.getMessage())
.append(", received value is '")
.append(constraintViolation.getInvalidValue())
.append("'; ");
}
return new Result.Builder<Void>().message(message.toString()).build();
}
}
2.4 示例请求
Request | Response |
http://localhost:8080/user?userId=111111 | // 正常情况 { "data": { "userName": "test user", "userId": "111111" }, "extraData": null, "message": null } |
// 异常情况 { "data": null, "extraData": null, "message": "Invalid Request Parameters: the length of userId must be 6 , received value is ''; userId must not be blank, received value is ''; " } |
3 原理解析
在前面,我们注册了一个MethodValidationPostProcessor这个Bean,那么这个Bean有什么用处呢?
3.1 MethodValidationPostProcessor
这个类是BeanPostProcessor接口(其可以在Bean初始化前或后做一些操作)的实现类,其内部持有一个JSR-303的provider,利用这个provider来执行方法级别的验证。
一般来说会使用行内验证注解,就像下面这样:
public @NotNull Object myValidMethod(@NotNull String arg1, @Max(10) int arg2)
如果上面的方法想要执行验证,就需要在目标类上添加Spring的@Validated注解。当然也可以在@Validated注解上指定group,默认是使用默认group。
注意:从Spring5.0开始,这个功能需要Bean Validation 1.1 Provider。
其代码如下:
public class MethodValidationPostProcessor extends AbstractBeanFactoryAwareAdvisingPostProcessor
implements InitializingBean {
// 默认的验证注解是@Validated
private Class<? extends Annotation> validatedAnnotationType = Validated.class;
// 持有一个validator实例,用以进行验证
@Nullable
private Validator validator;
// 验证注解也可以使用自定义的注解,并不是一定需要使用@Validated注解
public void setValidatedAnnotationType(Class<? extends Annotation> validatedAnnotationType) {
Assert.notNull(validatedAnnotationType, "'validatedAnnotationType' must not be null");
this.validatedAnnotationType = validatedAnnotationType;
}
// 设置JSR-303Validator,默认值是default ValidatorFactory中的default validator。
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,本质上还是去获取validator
public void setValidatorFactory(ValidatorFactory validatorFactory) {
this.validator = validatorFactory.getValidator();
}
// 要点1:Bean属性设置完后设置切面和通知
@Override
public void afterPropertiesSet() {
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
// 创建AOP增强,用以进行方法验证。切点是指定@Validated注解的地方,增强是一个MethodValidationInterceptor。
protected Advice createMethodValidationAdvice(@Nullable Validator validator) {
return (validator != null ? new MethodValidationInterceptor(validator) : new MethodValidationInterceptor());
}
}
这里使用了Spring AOP实现了增强,那么这个advice究竟做了什么呢?
3.2 MethodValidationInterceptor
这个类实现了MethodInterceptor接口,其内部持有一个validator实例,使用这个validator实例实现验证。其代码如下:
public class MethodValidationInterceptor implements MethodInterceptor {
// 持有validator实例,用以实现验证
private final Validator validator;
// 默认构造方法:使用默认的JSR-303 validator
public MethodValidationInterceptor() {
this(Validation.buildDefaultValidatorFactory());
}
// 构造方法:使用validatorFactory中的validator
public MethodValidationInterceptor(ValidatorFactory validatorFactory) {
this(validatorFactory.getValidator());
}
// 构造方法:使用指定的validator
public MethodValidationInterceptor(Validator validator) {
this.validator = validator;
}
// 要点2:实现MethodInterceptor中的invoke方法
@Override
@SuppressWarnings("unchecked")
public Object invoke(MethodInvocation invocation) throws Throwable {
// Avoid Validator invocation on FactoryBean.getObjectType/isSingleton
if (isFactoryBeanMetadataMethod(invocation.getMethod())) {
return invocation.proceed();
}
// 决定使用哪一个group进行验证
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
// 对方法中的参数进行验证
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// Probably a generic type mismatch between interface and impl as reported in SPR-12237 / HV-1011
// Let's try to find the bridged method on the implementation class...
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
// 如果方法不满足验证,则抛出ConstraintViolationException异常
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
// 调用方法,并获取方法返回值
Object returnValue = invocation.proceed();
// 对方法返回值进行验证,如果不满足验证,抛出ConstraintViolationException异常
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
// 判断方法是不是FactoryBean中的getObject和isSingleton方法
private boolean isFactoryBeanMetadataMethod(Method method) {
Class<?> clazz = method.getDeclaringClass();
// Call from interface-based proxy handle, allowing for an efficient check?
if (clazz.isInterface()) {
return ((clazz == FactoryBean.class || clazz == SmartFactoryBean.class) &&
!method.getName().equals("getObject"));
}
// Call from CGLIB proxy handle, potentially implementing a FactoryBean method?
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.getName(), method.getParameterTypes()));
}
// 决定使用哪一个validation group
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]);
}
}
这里我们已经看到JSR-303执行验证的代码,那么这个切面又是什么时候加进去的呢?
3.3 AbstractAdvisingBeanPostProcessor
看类名就知道这是个抽象类,并且实现了BeanPostProcessor接口。
其会在Bean初始化后,给特定的Bean应用Spring AOP。代码如下:
public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements BeanPostProcessor {
// 持有一个Advisor实例
@Nullable
protected Advisor advisor;
// 标记上面的advisor实例是否要放到其它advisor的前面
protected boolean beforeExistingAdvisors = false;
// 符合条件的Bean
private final Map<Class<?>, Boolean> eligibleBeans = new ConcurrentHashMap<>(256);
public void setBeforeExistingAdvisors(boolean beforeExistingAdvisors) {
this.beforeExistingAdvisors = beforeExistingAdvisors;
}
// 在bean初始化前不做任何事情
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
return bean;
}
// 要点3:给bean添加advice
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (this.advisor == null || bean instanceof AopInfrastructureBean) {
// Ignore AOP infrastructure such as scoped proxies.
return bean;
}
// 要点3-1:这个Bean不是原始Bean,已经添加过其它advice
if (bean instanceof Advised) {
Advised advised = (Advised) bean;
// 如果这个Advised实例没有被冻结,并且这个Bean符合要求,则添加advice
if (!advised.isFrozen() && isEligible(AopUtils.getTargetClass(bean))) {
// Add our local Advisor to the existing proxy's Advisor chain...
if (this.beforeExistingAdvisors) {
advised.addAdvisor(0, this.advisor);
}
else {
advised.addAdvisor(this.advisor);
}
return bean;
}
}
// 要点3-2:如果这个bean满足条件,则创建一个ProxyFactory,返回CGLIB代理对象
if (isEligible(bean, beanName)) {
ProxyFactory proxyFactory = prepareProxyFactory(bean, beanName);
if (!proxyFactory.isProxyTargetClass()) {
evaluateProxyInterfaces(bean.getClass(), proxyFactory);
}
proxyFactory.addAdvisor(this.advisor);
customizeProxyFactory(proxyFactory);
return proxyFactory.getProxy(getProxyClassLoader());
}
// No proxy needed.
return bean;
}
// 判断bean是否能够应用当前的advisor
protected boolean isEligible(Object bean, String beanName) {
return isEligible(bean.getClass());
}
// 判断给定的类是否能够应用advisor
protected boolean isEligible(Class<?> targetClass) {
Boolean eligible = this.eligibleBeans.get(targetClass);
if (eligible != null) {
return eligible;
}
if (this.advisor == null) {
return false;
}
eligible = AopUtils.canApply(this.advisor, targetClass);
this.eligibleBeans.put(targetClass, eligible);
return eligible;
}
// 为指定的bean准备ProcyFactory对象。子类可以在给ProxyFactory添加完advisor后对其进行修改。
protected ProxyFactory prepareProxyFactory(Object bean, String beanName) {
ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);
proxyFactory.setTarget(bean);
return proxyFactory;
}
// 子类可以选择实现这个方法来修改ProxyFactory
protected void customizeProxyFactory(ProxyFactory proxyFactory) {
}
}
参考
1. [苦B程序员的数据验证之路](http://www.mamicode.com/info-detail-323320.html)
2. [Java Bean Validation 最佳实践](https://www.cnblogs.com/beiyan/p/5946345.html)
3. [Spring Doc](https://docs.spring.io/spring/docs/4.3.18.RELEASE/spring-framework-reference/htmlsingle/#beans-factory-programmatically-registering-beanpostprocessors)
4. [Spring方法级别数据校验:@Validated + MethodValidationPostProcessor](https://segmentfault.com/a/1190000019891248?utm_source=tag-newest)