ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] # validation ## 基本使用 JSR实现,一个规范文档,指定一整套API,通过标注给对象属性添加约束. Hibernate Validator提供了JSR规范所有内置约束注解实现,以及一些附加的约束注解,用户还可以自定义注解. * @Valid参数前面添加@Valid注解,代表此对象使用了参数校验 * BindingResult参数校验的结果会存储在此对象中,可以根据属性判断是否校验通过,校验不通过可以把错误信息打印出来 1. 参数user前需要加上@Validated注解,表明需要spring对其进行校验,而校验的信息会存放到其后的BindingResult中。注意,必须相邻,如果有多个参数需要校验,形式可以如下。 ~~~ foo(@Validated Foo foo, BindingResult fooBindingResult ,@Validated Bar bar, BindingResult barBindingResult); ~~~ 即一个校验类对应一个校验结果。 2. 校验结果会被自动填充,在controller中可以根据业务逻辑来决定具体的操作,如跳转到错误页面 ~~~ import org.springframework.validation.BindingResult; import org.springframework.validation.ObjectError; @RequestMapping("/saveUser") public void saveUser(@Valid User user, BindingResult result) { System.out.println("user:" + user); if (result.hasErrors()) { List<ObjectError> list = result.getAllErrors(); for (ObjectError error : list) { System.out.println(error.getCode() + "-" + error.getDefaultMessage()); } } } ~~~ 需要定义一个接受的数据类型 其中, message="密码不不能为空"是自定义错误类型 ~~~ import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; @Data public class User { @NotEmpty(message = "姓名不能为空") private String name; @Max(value = 100, message = "年龄不能大于100岁") @Min(value = 18, message = "必须年满18岁") private int age; @NotEmpty(message = "密码不能为空") @Length(min = 6, message = "密码长度不能小于6位") private String pass; } ~~~ 测试 ~~~ String responseString = mockMvc.perform(MockMvcRequestBuilders.post("/saveUser") .param("name", "") .param("age", "666") .param("pass", "test") ).andReturn().getResponse().getContentAsString(); System.out.println(responseString); ~~~ ~~~ user:name=,age=666,pass=test Max-年年龄不不能⼤大于100岁 Length-密码⻓长度不不能⼩小于6位 NotEmpty-姓名不不能为空 ~~~ ## 部分标签含义 | 限制 | 说明 | | --- | --- | | @Valid | 被注释的元素是一个对象,需要检查此对象的所有字段值 | | @Null | 被注释的元素必须为 null | | @NotNull | 被注释的元素必须不为 null | | @AssertTrue | 被注释的元素必须为 true | | @AssertFalse | 被注释的元素必须为 false | | @Min(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | | @Max(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | | @DecimalMin(value) | 被注释的元素必须是一个数字,其值必须大于等于指定的最小值 | | @DecimalMax(value) | 被注释的元素必须是一个数字,其值必须小于等于指定的最大值 | | @Size(max, min) | 被注释的元素的大小必须在指定的范围内 | | @Digits (integer, fraction) | 被注释的元素必须是一个数字,其值必须在可接受的范围内 | | @Past | 被注释的元素必须是一个过去的日期 | | @Future | 被注释的元素必须是一个将来的日期 | | @Pattern(value) | 被注释的元素必须符合指定的正则表达式 | | @Email | 被注释的元素必须是电子邮箱地址 | | @Length(min, max) | 被注释的字符串的大小必须在指定的范围内 | | @NotEmpty | 被注释的字符串的大小必须在指定的范围内 | | @Range(min, max) | 被注释的元素必须在合适的范围内 | | @NotBlank | 被注释的字符串的必须非空 | | @URL(protocol, host, port, regexp, flags) | 被注释个 的字符串必须是一个有效的url | | @CreditCardNumber | 被注释的字符串必须通过Luhn校验算法,银行卡,信用卡等号码一般都用Luhn计算合法性 | | @ScriptAssert(lang, script, alias) | 要有Java Scripting API 即JSR 223 ("Scripting for the JavaTM Platform")的实现 | | @SafeHtml(whitelistType, additionalTags) | classpath中要有jsoup包 | ~~~ @Pattern(regexp="^[a-zA-Z0-9]+$",message="{account.username.space}") @Size(min=3,max=20,message="{account.username.size}") ~~~ # 自定义校验 校验手机号或身份证号,官方提供的注解中没有支持的,当然我们可以通过官方提供的正则表达式来校验: ~~~ @Pattern(regexp = "^1(3|4|5|7|8)\\d{9}$",message = "手机号码格式错误") @NotBlank(message = "手机号码不能为空") private String phone; ~~~ **定义手机号校验注解 @Phone** ~~~ @Target({ ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = PhoneValidator.class) public @interface Phone { /** * 校验不通过的message */ String message() default "请输入正确的手机号"; /** * 分组校验 */ Class<?>[] groups() default { }; Class<? extends Payload>[] payload() default { }; } ~~~ **定义校验方式** 想让自定义验证注解生效,需要实现`ConstraintValidator`接口。 接口的第一个参数是 **自定义注解类型**,第二个参数是**被注解字段的类型** 需要提到的一点是`ConstraintValidator`接口的实现类无需添加`@Component`它在启动的时候就已经被加载到容器中了 ~~~ public class PhoneValidator implements ConstraintValidator<Phone, String> { @Override public void initialize(Phone constraintAnnotation) { } @Override public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) { if(!StringUtils.isEmpty(phone)){ //获取默认提示信息 String defaultConstraintMessageTemplate = constraintValidatorContext.getDefaultConstraintMessageTemplate(); System.out.println("default message :" + defaultConstraintMessageTemplate); //禁用默认提示信息 constraintValidatorContext.disableDefaultConstraintViolation(); //设置提示语 constraintValidatorContext.buildConstraintViolationWithTemplate("手机号格式错误").addConstraintViolation(); String regex = "^1(3|4|5|7|8)\\d{9}$"; return phone.matches(regex); } return true; } } ~~~ # 分组校验 当发生多个错误,spring validation不会在第一个错误发生后立即停止,而是继续试错,告诉我们所有的错误。 如果同一个类,在不同的使用场景下有不同的校验规则,那么可以使用分组校验。未成年人是不能喝酒的,而在其他场景下我们不做特殊的限制,这个需求如何体现同一个实体,不同的校验规则呢? 改写注解,添加分组: ~~~ Class Foo{ @Min(value = 18,groups = {Adult.class}) private Integer age; public interface Adult{} public interface Minor{} } ~~~ 这样表明,只有在Adult分组下,18岁的限制才会起作用。 Controller层改写: drink方法限定需要进行Adult校验,而live方法则不做限制。 ~~~ @RequestMapping("/drink") public String drink(@Validated({Foo.Adult.class}) Foo foo, BindingResult bindingResult) { if(bindingResult.hasErrors()){ for (FieldError fieldError : bindingResult.getFieldErrors()) { //... } return "fail"; } return "success"; } @RequestMapping("/live") public String live(@Validated Foo foo, BindingResult bindingResult) { if(bindingResult.hasErrors()){ for (FieldError fieldError : bindingResult.getFieldErrors()) { //... } return "fail"; } return "success"; } ~~~ # 统一错误信息 定义错误信息 在resources/下创建名称为ValidationMessages.properties 注意:名字必须为“ValidationMessages.properties“ 因为SpringBoot自动读取classpath中的ValidationMessages.properties里的错误信息 value 为提示信息 ,但是是ASCII 。(内容为“名字不能为空“) ~~~ # 中文是不行的 member.mid.notnull.error=用户名不允许为空 member.mid.email.error=用户名的注册必须输入正确的邮箱 member.mid.length.error=用户名的格式错误 ~~~ ![](https://img.kancloud.cn/79/34/7934b50c613cb4413e6e4bcbf6397d6f_1456x72.png) ~~~ @NotNull(message="{member.mid.notnull.error}") @Email(message="{member.mid.email.error}") @Length(min=6,message="{member.mid.length.error}") private String mid; ~~~ ## 自定义异常处理器 当验证不通过时会抛异常出来,在全局异常中定义异常处理器。 捕获异常信息(因为验证不通过的项可能是多个所以统一捕获处理),并抛给前端。 ![](https://img.kancloud.cn/4b/cd/4bcdbf55a24a9ce7d6c2263190ecd4d5_1694x240.png) @RestControllerAdvice @ExceptionHandler(写你想要拦截的异常类型) **这两个注解必须要** 第一个方法是校验异常的统一处理 第二个是防止参数类型不一致的处理 当然你也可以在里面处理其他异常。 ~~~ @RestControllerAdvice public class BadRequestExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(BadRequestExceptionHandler.class); /** * 校验错误拦截处理 * * @param exception 错误信息集合 * @return 错误信息 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ServiceResult validationBodyException(MethodArgumentNotValidException exception){ BindingResult result = exception.getBindingResult(); if (result.hasErrors()) { List<ObjectError> errors = result.getAllErrors(); errors.forEach(p ->{ FieldError fieldError = (FieldError) p; logger.error("Data check failure : object{"+fieldError.getObjectName()+"},field{"+fieldError.getField()+ "},errorMessage{"+fieldError.getDefaultMessage()+"}"); }); } return ServiceResult.error("请填写正确信息"); } /** * 参数类型转换错误 * * @param exception 错误 * @return 错误信息 */ @ExceptionHandler(HttpMessageConversionException.class) public ServiceResult parameterTypeException(HttpMessageConversionException exception){ logger.error(exception.getCause().getLocalizedMessage()); return ServiceResult.error("类型转换错误"); } } ~~~ # @Validated和@Valid的区别 * @Valid是使用Hibernate validation的时候使用 * @Validated是只用Spring Validator校验机制使用 说明:java的JSR303声明了@Valid这类接口,而`Hibernate-validator`对其进行了实现 @Validation对@Valid进行了二次封装,在使用上并没有区别,但在分组、注解位置、嵌套验证等功能上有所不同,这里主要就这几种情况进行说明。 **注解位置** * @Validated:用在类型、方法和方法参数上。但不能用于成员属性(field) * @Valid:可以用在方法、构造函数、方法参数和成员属性(field)上 **分组校验** * @Validated:提供分组功能,可以在参数验证时,根据不同的分组采用不同的验证机制 * @Valid:没有分组功能 举例: 定义分组接口: ~~~ public interface IGroupA {} public interface IGroupB {} ~~~ 定义需要检验的参数bean: ~~~ public class StudentBean implements Serializable{ @NotBlank(message = "用户名不能为空") private String name; //只在分组为IGroupB的情况下进行验证 @Min(value = 18, message = "年龄不能小于18岁", groups = {IGroupB.class}) private Integer age; @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误") private String phoneNum; @Email(message = "邮箱格式错误") private String email; @MyConstraint private String className; ~~~ 测试代码: 检验分组为IGroupA的情况 ~~~ @RestController public class CheckController { @PostMapping("stu") public String addStu(@Validated({IGroupA.class}) @RequestBody StudentBean studentBean){ return "add student success"; } } ~~~ 这里对分组IGroupB的就没检验了 如果把测试代码改成下面这样,看看测试结果 ~~~ @RestController public class CheckController { @PostMapping("stu") public String addStu(@Validated({IGroupA.class, IGroupB.class}) @RequestBody StudentBean studentBean){ return "add student success"; } } ~~~ 说明: 1. 不分 配groups,默认每次都要进行验证 2. 对一个参数需要多种验证方式时,也可通过分配不同的组达到目的。 # 组序列 默认情况下 不同级别的约束验证是无序的,但是在一些情况下,顺序验证却是很重要。 一个组可以定义为其他组的序列,使用它进行验证的时候必须符合该序列规定的顺序。在使用组序列验证的时候,如果序列前边的组验证失败,则后面的组将不再给予验证。 **定义组序列** ~~~ @GroupSequence({Default.class, IGroupA.class, IGroupB.class}) public interface IGroup { } ~~~ 需要校验的Bean,分别定义IGroupA对age进行校验,IGroupB对className进行校验: ~~~ public class StudentBean implements Serializable{ @NotBlank(message = "用户名不能为空") private String name; @Min(value = 18, message = "年龄不能小于18岁", groups = IGroupA.class) private Integer age; @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误") private String phoneNum; @Email(message = "邮箱格式错误") private String email; @MyConstraint(groups = IGroupB.class) private String className; ~~~ 测试代码 ~~~ @RestController public class CheckController { @PostMapping("stu") public String addStu(@Validated({IGroup.class}) @RequestBody StudentBean studentBean){ return "add student success"; } } ~~~ 测试发现,如果age出错,那么对组序列在IGroupA后的IGroupB不进行校验,即例子中的className不进行校验 # 嵌套校验 一个待验证的pojo类,其中还包含了待验证的对象,需要在待验证对象上注解@Valid,才能验证待验证对象中的成员属性,这里不能使用@Validated。 需要约束校验的bean: ~~~ public class TeacherBean { @NotEmpty(message = "老师姓名不能为空") private String teacherName; @Min(value = 1, message = "学科类型从1开始计算") private int type; ~~~ ~~~ public class StudentBean implements Serializable{ @NotBlank(message = "用户名不能为空") private String name; @Min(value = 18, message = "年龄不能小于18岁") private Integer age; @Pattern(regexp = "^((13[0-9])|(14[5,7,9])|(15([0-3]|[5-9]))|(166)|(17[0,1,3,5,6,7,8])|(18[0-9])|(19[8|9]))\\d{8}$", message = "手机号格式错误") private String phoneNum; @Email(message = "邮箱格式错误") private String email; @MyConstraint private String className; @Valid @NotNull(message = "任课老师不能为空") @Size(min = 1, message = "至少有一个老师") private List<TeacherBean> teacherBeans; ~~~