@Valid和@Validator, hibernate-validator校验多重嵌套dto参数不生效的问题解决
一. 关于hibernate-validator
这个是针对mvc参数校验的一个框架,不过相信大家看这个文章的自然是知道是做什么用的。可以看我的这篇使用hibernate-validator做参数校验的文章
二. 嵌套实体校验不生效
不过@Valid和@Validator针对多重嵌套的dto没办法很好的校验
比如A类
, 有个字段是List<B> bs
, 我们可以用@Valid和@Validator去校验A类中的字段, 比如@NotEmpty, @NotNull等。 但是会发现B类中加了这些注解不生效。
在网上搜索的结果是,在List<B> bs
上加注解@Valid, 在A上加注解@Validator。 确实可行, 但是@Valid没有group分组功能。比如:
public void save(@Validator(SaveGroup.class) @RequestBody A a){}
public void update(@Validator(UpdateGroup.class) @RequestBody A a){}
public class A{
...
@Valid
private List<B> bs;
}
public class B{
@NotNull(UpdateGroup.class)
private Integer id;
@NotNull(SaveGroup.class)
private String name;
}
如上所示, 我们会发现, 虽然在List<B>
上加了注解能让B中的校验生效, 但是分组功能却无法使用. 我们想要在save的时候判断name不为空, update的时候判断id不为空.
三. 拦截器方案解决
这里使用拦截器来改造下这个校验功能。hibernate-validator提供了工具类让我们能自己校验参数:
ValidatorUtils
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import java.util.Set;
/**
* hibernate-validator校验工具类
*
* 参考文档:http://docs.jboss.org/hibernate/validator/5.4/reference/en-US/html_single/
*
* @author zzzgd
*/
public class ValidatorUtils {
private static Validator validator;
static {
validator = Validation.buildDefaultValidatorFactory().getValidator();
}
/**
* 校验对象
* @param object 待校验对象
* @param groups 待校验的组
* @throws BusinessException 校验不通过,则报RRException异常
*/
public static void validateEntity(Object object, Class<?>... groups)
throws BusinessException {
Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
if (!constraintViolations.isEmpty()) {
ConstraintViolation<Object> constraint = constraintViolations.iterator().next();
throw new BusinessException(constraint.getMessage());
}
}
}
自定义注解
InnerValidator
/**
* InnerValidator
*
* @author zgd
* @date 2020/9/27 16:38
*/
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface InnerValidator {
Class<?>[] value() default {};
}
拦截器
ParamInterceptor
package com.zzzgd.aop.interceptor;
import cn.hutool.core.annotation.AnnotationUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ReflectUtil;
import com.zzzgd.valid.ValidatorUtils;
import com.zzzgd.anno.InnerValidator;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodIntrospector;
import org.springframework.core.MethodParameter;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.InvocableHandlerMethod;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.ServletRequestDataBinderFactory;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* ParamInterceptor
*
* @author zgd
* @date 2020/9/27 16:33
*/
public class ParamInterceptor implements HandlerInterceptor, InitializingBean {
@Autowired
private RequestMappingHandlerAdapter adapter;
private List<HandlerMethodArgumentResolver> argumentResolvers;
// 缓存
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
/**
* 这里换成自己的代码的基本包名,只校验这个包下的dto
*/
private final static String BASE_PACKAGE = "com.zzzgd";
@Override
public void afterPropertiesSet() {
argumentResolvers = adapter.getArgumentResolvers();
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 只处理HandlerMethod方式
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
// 拿到该方法所有的参数们~~~ org.springframework.core.MethodParameter
MethodParameter[] parameters = method.getMethodParameters();
if (parameters.length == 0) {
return true;
}
//遍历所有的入参:给每个参数做赋值和数据绑定
for (MethodParameter parameter : parameters) {
Valid valid = parameter.getParameterAnnotation(Valid.class);
Validated validated = parameter.getParameterAnnotation(Validated.class);
if (valid == null && validated == null) {
continue;
}
Class<?>[] groups = {};
if (validated != null) {
groups = validated.value();
}
Object value = getValue(parameter, request, response, method);
validInnerField(parameter.getParameterType(), value, groups);
}
}
return true;
}
private void validInnerField(Class clazz, Object value, Class<?>[] groups) {
if (value == null || !isCustomClass(clazz)) {
//如果值是null无需嵌套实体dto,或者类型是jdk自带类型,不可能在jdk自带类型的字段上加校验参数的注解
return;
}
Field[] fields = ReflectUtil.getFields(clazz);
for (Field field : fields) {
Object fieldValue = ReflectUtil.getFieldValue(value, field);
Class<?> fieldType = field.getType();
Type genericType = field.getGenericType();
if (!isCustomClass(getSuperClassGenricType(genericType))) {
//如果字段类型是 jdk自带类型,也不符合嵌套dto,并且允许在字段上加注解的情况
continue;
}
InnerValidator annotation = AnnotationUtil.getAnnotation(field, InnerValidator.class);
//校验该字段的值
if (fieldValue != null) {
if (annotation != null) {
Class<?>[] groups2 = annotation.value();
//如果该字段上的InnerValidator有标注分组,则按远近原则, 优先以这个分组为准
groups = groups2.length == 0 ? groups : groups2;
}
if (fieldValue instanceof Iterable) {
//是集合list
for (Object ov : ((Iterable) fieldValue)) {
fieldType = ov.getClass();
if (annotation != null) {
//校验其中的值
ValidatorUtils.validateEntity(ov, groups);
}
//递归判断这个字段实体中,是否还有这个注解
validInnerField(fieldType, ov, groups);
}
} else {
if (annotation != null) {
//校验其中的值
ValidatorUtils.validateEntity(fieldValue, groups);
}
validInnerField(fieldType, fieldValue, groups);
}
}
}
}
/**
* 判断一个类是JAVA类型还是用户定义类型
*
* @param clz
* @return
*/
public static boolean isJdkClass(Class<?> clz) {
return clz != null && clz.getClassLoader() == null;
}
public static boolean isCustomClass(Class<?> clz) {
return clz != null && clz.getName().startsWith(BASE_PACKAGE);
}
private Object getValue(MethodParameter parameter, HttpServletRequest request, HttpServletResponse response, HandlerMethod method) throws Exception {
// 找到适合解析这个参数的处理器~
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
WebDataBinderFactory webDataBinderFactory = getDataBinderFactory(method);
return resolver.resolveArgument(parameter, mavContainer, new ServletWebRequest(request, response), webDataBinderFactory);
}
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 支持到@InitBinder注解
methods = MethodIntrospector.selectMethods(handlerType, RequestMappingHandlerAdapter.INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(new InvocableHandlerMethod(bean, method));
}
return new ServletRequestDataBinderFactory(initBinderMethods, adapter.getWebBindingInitializer());
}
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
public static Class getSuperClassGenricType(Type type) throws IndexOutOfBoundsException {
if (!(type instanceof ParameterizedType)) {
return Object.class;
}
Type[] params = ((ParameterizedType) type).getActualTypeArguments();
if (params.length == 1) {
return (Class) params[0];
}
for (Type param : params) {
if (!(param instanceof Class)) {
return Object.class;
}
}
return Object.class;
}
}
配置拦截器
WebMvcConfig
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
// 配置用于校验的拦截器
@Bean
public ParamInterceptor validationInterceptor() {
return new ParamInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(validationInterceptor()).addPathPatterns("/**");
}
}
三. 使用
- 可以修改拦截器中
validInnerField
方法中, 是调用isCustomClass
还是isJdkClass
, 前者指定包路径, 只校验这个包下面的dto, 后者只校验非jdk的类。因为我们用来加@NotEmpty, @NotNull的这些类肯定不是这些class - 在Controller方法中,对最外层的dto加@Valid或者@Validator注解, 那么这个dto中有@InnerValidator注解的字段都会被校验。比如上面的
A
类和B
类,甚至B
类有个字段List<C>
,那么只要在List<C>
字段上加@InnerValidator注解即可 - 分组也是可以沿用的。比如Controller方法中@Validator注解写明了分组是SaveGroup,那么其中嵌套的@InnerValidator注解的字段也会按这个分组校验,也可以在@InnerValidator注解中标特定的分组类。
- 目前初步是用这个来实现,后续有优化或修复会同步更新