<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
验证注解 | 验证的数据类型说明 |
---|---|
@AssertFalse | Boolean,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、数组等 |
@Past | java.util.Date,java.util.Calendar;Joda Time类库的日期类型 |
@Future | java.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;
比如用户对象,在页面可以进行新增和修改,假设用户对象有字段: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修饰参数时,表示要验证这个参数对象,但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 );
}
功能需求:假设自定义注解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流进行完善。