javax.validation优雅的数据校验和spring参数解析器的使用

闽念
2023-12-01

maven依赖

<dependency>    
	<groupId>javax.validation</groupId>    
	<artifactId>validation-api</artifactId>    
	<version>1.1.0.Final</version>
</dependency>
<!-- hibernate validator-->
<dependency>    
	<groupId>org.hibernate</groupId>    
	<artifactId>hibernate-validator</artifactId>    
	<version>5.3.5.Final</version>
</dependency>
# 如果系统是用springboot构建的话,直接引入starter-web即可,如下所示:
<dependency>   
		<groupId>org.springframework.boot</groupId>   
		 <artifactId>spring-boot-starter-web</artifactId> 
		 <version>1.5.5.RELEASE</version>
 </dependency>
 # 依赖关系如下:
 +--- org.springframework.boot:spring-boot-starter-web -> 1.5.5.RELEASE|    
   +--- org.hibernate:hibernate-validator:5.3.5.Final -> 5.3.4.Final|    |   
     +--- javax.validation:validation-api:1.1.0.Final

注解说明

验证注解验证的数据类型说明
@AssertFalseBoolean,boolean
@NotNull任意类型
@Null任意类型
@Min(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型
@DecimalMin(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型
@DecimalMax(value=值)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型
@Digits(integer=整数位数, fraction=小数位数)BigDecimal,BigInteger, byte,short, int, long,等任何Number或CharSequence(存储的是数字)子类型
@Size(min=下限, max=上限)字符串、Collection、Map、数组等
@Pastjava.util.Date,java.util.Calendar;Joda Time类库的日期类型
@Futurejava.util.Date,java.util.Calendar;Joda Time类库的日期类型
@Length(min=下限, max=上限)CharSequence子类型
@Range(min=最小值, max=最大值)BigDecimal,BigInteger,CharSequence, byte, short, int, long等原子类型和包装类型
@Email(regexp=正则表达式,flag=标志的模式)CharSequence子类型(如String)
@Pattern(regexp=正则表达式,flag=标志的模式)String,任何CharSequence的子类型
@Valid任何非原子类型

示例

## 实体类
@Data
public class UserDto implements Serializable {    
	private static final long serialVersionUID = 1L;    
	/*** 用户ID*/    
	@NotNull(message = "用户id不能为空")    
	private Long userId;    
	/** 用户名*/    
	@NotBlank(message = "用户名不能为空")    
	@Length(max = 20, message = "用户名不能超过20个字符")    
	@Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "用户昵称限制:最多20字符,包含文字、字母和数字")    
	private String username;    
	/** 手机号*/    
	@NotBlank(message = "手机号不能为空")    
	@Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手机号格式有误")    
	private String mobile;   
	 /**性别*/    
	 private String sex;    
	 /** 邮箱*/    
	 @NotBlank(message = "联系邮箱不能为空")    
	 @Email(message = "邮箱格式不对")    
	 private String email;    
	 /** 密码*/    
	 private String password;    
	 /*** 创建时间 */    
	 @Future(message = "时间必须是将来时间")    
	 private Date createTime;
 }
## controller类
@RestController@RequestMapping("/test")//form表单验证时需要@Validated
public class TestController {    
	/**     
	* @Validated  这个注解必须加上,不然不会自动验证     
	* @param userDto     
	* @return     
	*/   
	@PostMapping("/save")    
	public ResponseMessage save(@RequestBody @Validated UserDto userDto){        
	System.out.println(userDto.getUserId());        
	return null;    
	}    
	/**     
	* 这种form表单方式,需要在类上加入@Validated注解     
	* @param userName     
	* @return     
	*/    
	@PostMapping("/save1")    
	public ResponseMessage save1(@RequestParam("userName") @NotBlank(message = "用户名不能为空") String userName){
		System.out.println(userName);        
		return null;    
   }
}
## 统一异常处理类
@ControllerAdvice
@Slf4j
public class ControllerExceptionHandler {    

	@ResponseBody    
	@ExceptionHandler(value = MethodArgumentNotValidException.class)
	public ResponseMessage errorHandler(MethodArgumentNotValidException ex) { 
		List<FieldError> fieldErrors = ex.getBindingResult().getFieldErrors();        
		StringBuilder stringBuilder = new StringBuilder();
		for(FieldError error:fieldErrors){  
			stringBuilder.append(error.getDefaultMessage()+",");        
		}        
		log.error("出现系统异常,异常信息为{}",stringBuilder.toString());        
		return new ResponseMessage(Constants.FAILURE_CODE, "",stringBuilder.toString());    
	}    
	@ResponseBody    
	@ExceptionHandler(ConstraintViolationException.class)    
	public ResponseMessage handleConstraintViolationException(ConstraintViolationException e) {
		Set<ConstraintViolation<?>> errors = e.getConstraintViolations();
		StringBuilder stringBuilder = new StringBuilder();
		for(ConstraintViolation error:errors){
			stringBuilder.append(error.getMessage()+",");
		}        
		log.error("出现系统异常,异常信息为{}",stringBuilder.toString());        
		return new ResponseMessage(Constants.FAILURE_CODE,"", stringBuilder.toString());    
	}    
}

自定义参数注解

# 比如我们来个 自定义身份证校验 注解
@Documented@Target({ElementType.PARAMETER, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = IdentityCardNumberValidator.class)
public @interface IdentityCardNumber {
   String message() default "身份证号码不合法";
   Class<?>[] groups() default {};
   Class<? extends Payload>[] payload() default {};
}
#注解处理类
public class IdentityCardNumberValidator implements ConstraintValidator<IdentityCardNumber, Object> {
   @Override
   public void initialize(IdentityCardNumber identityCardNumber) {
   }
   @Override
   public boolean isValid(Object o, ConstraintValidatorContext constraintValidatorContext) {
   return IdCardValidatorUtils.isValidateIdcard(o.toString());
   }
}
#使用自定义注解
@NotBlank(message = "身份证号不能为空")
@IdentityCardNumber(message = "身份证信息有误,请核对后提交")
private String clientCardNo;

使用groups的校验

比如用户对象,在页面可以进行新增和修改,假设用户对象有字段:id,userName,password。新增页面id为空,修改页面id字段不能为空,其他字段都不能为空,不可能针对这种场景写2个用户对象吧。

#先定义groups的分组接口Create和Updateimport 
javax.validation.groups.Default;
public interface Create extends Default {}

import javax.validation.groups.Default;
public interface Update extends Default{}

###注意:这里的Create和Update必须继承Default

#再在需要校验的地方用@Validated声明校验组
@PostMapping("/save")
public ResponseMessage save(@RequestBody @Validated(Create.class) UserDto userDto){
    System.out.println(userDto.getUserId());
    return null;
}
@PostMapping("/update")
public ResponseMessage update(@RequestBody @Validated(Update.class) UserDto userDto){
    System.out.println(userDto.getUserId());
    return null;
}

#在DTO中的字段上定义好groups = {}的分组类型
@Data
public class UserDTO implements Serializable {
    /*** 用户ID @Validated(Update.class)时不能为空*/
    @NotNull(message = "用户id不能为空",groups = Update.class)
    private Long userId;
    /** 用户名 @Validated(Update.class) @Validated(Create.class) @Validated 时都不能为空*/
    @NotBlank(message = "用户名不能为空",groups = {Create.class,Update.class})
    private String username;
    /** 密码 @Validated(Update.class) @Validated(Create.class) @Validated 时都不能为空*/
    @NotBlank(message = "密码不能为空")
    private String password;
}

手动调用对象属性数据验证

public class ParamValidatorUtil {
	private static Validator validator;
	static {
		validator = Validation.buildDefaultValidatorFactory().getValidator();
	}
	public static String validateEntity(Object object, Class<?>... groups)
	throws BusinessException {
		Set<ConstraintViolation<Object>> constraintViolations = validator.validate(object, groups);
		if (!constraintViolations.isEmpty()) {
		StringBuilder msg = new StringBuilder();
		for (ConstraintViolation<Object> constraint : constraintViolations) {
			msg.append(constraint.getMessage()+",");
		}            
			return msg.toString();
		} 
		return null;
	}
}

Validated注解修饰方法参数-源码解析

当注解Validated修饰参数时,表示要验证这个参数对象,但spring如何做到的呢?入口处为类HandlerMethodArgumentResolverComposite。

##类HandlerMethodArgumentResolverComposite 实现了接口 HandlerMethodArgumentResolver,并重写了方法supportsParameter和resolveArgument

public boolean supportsParameter(MethodParameter parameter) {
	//该方法返回true时,表示要执行接口HandlerMethodArgumentResolver的resolveArgument方法 
	return (getArgumentResolver(parameter) != null);
}

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
	//parameter参数为的某个方法的某个参数  webRequest为servletwebrequest对象    
	//获取方法参数解析器 
	HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
	if (resolver == null) {
		throw new IllegalArgumentException("Unknown parameter type [" + parameter.getParameterType().getName() + "]");
	} 
	//利用参数解析器解析方法,返回对象为方法参数对应的对象 
	return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

#执行上述方法最终会调用RequestResponseBodyMethodProcessor类,此类也实现了接口HandlerMethodArgumentResolver
public boolean supportsParameter(MethodParameter parameter) {
	//参数是否有注解RequestBody 
	return parameter.hasParameterAnnotation(RequestBody.class);
}

public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,  NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
	//controller的某个方法的某个参数
	parameter = parameter.nestedIfOptional();
	//controller的某个方法的某个参数对应的示例 如例子中的UserDto实例 
	Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
	//controller的某个方法的某个参数对应的类型首字母小写,如userDto 
	String name = Conventions.getVariableNameForParameter(parameter); 
	WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
	if (arg != null) {
		//参数验证  
		validateIfApplicable(binder, parameter);
		if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
			throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());  
		} 
	} 
	mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult()); 
	return adaptArgumentIfNecessary(arg, parameter);
}

protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
	//获取参数的所有注解,并遍历注解 
	Annotation[] annotations = parameter.getParameterAnnotations(); 
	for (Annotation ann : annotations) {
		Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
		//如果参数的注解为Validated  
		if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
			Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
			Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints}); 
			//最终调用ValidatorImpl类的validate方法去验证参数   
			binder.validate(validationHints); 
			break;
		} 
	}
}

# ValidatorImpl类的validate方法 这个类已经不是spring包的类了,而是hibernate包的类了
public final <T> Set<ConstraintViolation<T>> validate(T object, Class<?>... groups) { 
	Contracts.assertNotNull( object, MESSAGES.validatedObjectMustNotBeNull() ); 
	if ( !beanMetaDataManager.isConstrained( object.getClass() ) ) {  
		return Collections.emptySet(); 
	} 
	ValidationOrder validationOrder = determineGroupValidationOrder( groups ); 
	ValidationContext<T> validationContext = getValidationContext().forValidate( object ); 
	ValueContext<?, Object> valueContext = ValueContext.getLocalExecutionContext(   object,   beanMetaDataManager.getBeanMetaData( object.getClass() ),   PathImpl.createRootPath() ); 
	return validateInContext( valueContext, validationContext, validationOrder );
}

自定义注解实现类似参数Validated注解

功能需求:假设自定义注解MD5Encryp加密注解,当controller方法中的参数被这个注解修饰时,如果该对象是一个字符串,则直接对这个字符串进行MD5加密,如果是某个对象,则将该对象的所有字段进行MD5加密

#定义注解
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MD5Encryp {}

# 定义参数解析器
public class MD5EncrypHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        // 参数被注解MD5Encryp修饰的对象才被本解析器解析
        return parameter.hasParameterAnnotation(MD5Encryp.class);
    }
    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
        //参数类型
        Class<?> paramType = parameter.getParameterType();
        //获取请求参数,如果参数类型为字符串,则直接返回,如果为对象,则转换成对应的对象
        //注意:这里为了简单起见,前端是以form表单的方式进行请求
        Map<String, String[]> parameterMap = webRequest.getParameterMap();
        Map<String, Object> result = new LinkedHashMap(parameterMap.size());
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            if (entry.getValue().length > 0) {
                result.put(entry.getKey(), MD5Util.handle(entry.getValue()[0]));
            }
        }

        if(paramType.isAssignableFrom(String.class) ){
            return result.values().iterator().next();
        }else{
            return JSONObject.toJavaObject(new JSONObject(result), paramType);
        }
    }
}

# 为spring添加参数解析1
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
       argumentResolvers.add(new MD5EncrypHandlerMethodArgumentResolver());
    }
}

# controller
@PostMapping("/save")
public ResponseMessage save(@MD5Encryp UserDto userDto){
    System.out.println(userDto.getUserId());
    return null;
}
## 注意:controller这里的userdto只能有MD5Encryp这个修饰,如果还有RequestBody注解修饰,则进不了自定义的解析器里
## 因为spring为我们提供了很多解析器会被系统优先处理,而所有的解析器组成的解析器链,都是一个执行完后,其他的就不会被执行了。
## 如果要使用我们自定义的解析器,而不管该参数是否被其他注解修饰,则需要进行如下处理:

# 为spring添加解析2
@Configuration
@Slf4j
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    //@Override 这里需要注释掉
    //public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    //   argumentResolvers.add(new MD5EncrypHandlerMethodArgumentResolver());    
	//}
	
    @Autowired     
	private RequestMappingHandlerAdapter adapter;    
	@PostConstruct
    public void injectSelfMethodArgumentResolver() {
        List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();
        //优先加载自定义的解析器
        argumentResolvers.add(new MD5EncrypHandlerMethodArgumentResolver());
        //再加载系统自定义的解析器
        argumentResolvers.addAll(adapter.getArgumentResolvers());
        adapter.setArgumentResolvers(argumentResolvers);
     }
}
 
 # RequestBody注解将无效
 @PostMapping("/save")
 public ResponseMessage save(@MD5Encryp @RequestBody UserDto userDto){
     System.out.println(userDto.getUserId());
     return null;
 }
 
 ### 上述例子里,尤其是参数解析器解析参数的代码比较粗糙,如果要完成类似@RequestBody和@RequestParm的功能,需要结合可复制的request流进行完善。
 
 类似资料: