该功能由[`Hibernate-validator`](http://hibernate.org/validator/releases/6.0/)封装而来 > 在任何时候,当你要处理一个应用程序的业务逻辑,数据校验是你必须要考虑和面对的事情。应用程序必须通过某种手段来确保输入进来的数据从语义上来讲是正确的,通常我们会有一个验证数据的过程,待这些验证过程完毕,结果无误后,参数才会进入到正式的业务处理中。 > 我们在开发过程中,会有各种各样的入参,会经常需要写一些校验的代码,比如字段非空,字段长度限制,邮箱格式验证等等,写这些与业务逻辑关系不大的代码个人感觉不够优雅: > * 验证代码重复繁琐 > * 方法内代码冗余 `Hibernate-validator`应运而生,与持久层框架`Hibernate`没有什么关系,是对`JSR 380(Bean Validation 2.0)`、`JSR 303(Bean Validation 1.0)`规范的实现;部分注解如下: ![](https://img.kancloud.cn/3e/ad/3eadf7af6b59f72bd5ccfe3892eed30c_947x699.png) ### 依赖模块 ``` <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> </dependency> ``` > 下边主要从4个方面如何去使用; > * 1、简单校验 > * 2、分组校验 > * 3、自定义校验 > * 4、对象校验 ### 1、简单校验 方式1:走的是 Hibernate校验,失败则抛出`ConstraintViolationException`异常 TestController.java ``` @RestController @RequestMapping(value = "/api") @Validated public class TestController{ @RequestMapping("/test") public BaseResult test(@NotNull(message = "参数不能为空") String key) { return R.succ(key); } } ``` 方式1:校验结果 ``` [FBOOT][ERROR][08-29 17:19:55]-->[http-nio-9090-exec-3:39648][validatorException(GlobalExceptionAdvice.java:84)] | - validatorException ...... javax.validation.ConstraintViolationException: test.key: 参数不能为空 at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:116) ~[spring-context-5.2.8.RELEASE.jar:5.2.8.RELEASE] ``` ``` { "code": 3000, "msg": "效验错误", "data": [ "test.key 参数不能为空" ], "success": false } ``` 方式2:走的是 Spring校验,失败则抛出`BindException`异常 TestController.java ``` @RestController @RequestMapping(value = "/api") public class TestController{ @RequestMapping("/test") public BaseResult test(@Validated TestVo vo) { return R.succ(vo.getKey()); } } ``` TestVo.java ``` @Data public class TestVo { @NotNull(message = "参数不能为空") private String key; } ``` 方式2:校验结果 ``` [FBOOT][ERROR][08-29 17:28:30]-->[http-nio-9090-exec-1:24367][validatorException(GlobalExceptionAdvice.java:84)] | - validatorException ...... org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors Field error in object 'testVo' on field 'key': rejected value [null]; codes [NotNull.testVo.key,NotNull.key,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [testVo.key,key]; arguments []; default message [key]]; default message [参数不能为空] at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:164) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE] ``` ``` { "code": 3000, "msg": "效验错误", "data": [ "key 参数不能为空" ], "success": false } ``` 不管是方式1/2都,校验失败后会被全局`异常捕获`处理,再`统一返回`规定格式 ### 2、分组校验 实际开发场景中: > 查询用户接口A,必传:id > 修改昵称接口B,必传:id,nickname > 修改手机接口C,必传:id,code,phone 假设我们每类业务逻辑都有一个VO来接收参数,每个定义N个字段,但每次操作并不需要所有都是必传,可能只是个别参数必传,如何优雅的解决? TestVo.java ``` @Data public class TestVo { @NotNull(message = "用户ID不能为空") private String id; @NotNull(message = "昵称不能为空", groups = B.class) private String nickname; @NotNull(message = "验证码不能为空", groups = C.class) private String code; @NotNull(message = "手机不能为空", groups = C.class) private String phone; public interface TestVoValid { public interface A{} public interface B{} public interface C{} } } ``` TestController.java ``` @RestController @RequestMapping(value = "/api") public class TestController{ // 校验,默认分组 Default.class (注:没有使用分组A,默认使用Default) @RequestMapping("/test") public BaseResult test(@Validated TestVo vo) { return R.succ(vo.getId()); } // 校验,默认分组 Default.class + B.class @RequestMapping("/update") public BaseResult update(@Validated(value = {Default.class, B.class}) TestVo vo) { return R.succ(vo.getNickname()); } // 校验,默认分组 Default.class + C.class @RequestMapping("/change") public BaseResult change(@Validated(value = {Default.class, C.class}) TestVo vo) { return R.succ(vo.getPhone()); } } ``` 校验结果 > 本框架默认是`快速模式`即只要有一个不通过就返回; > 原默认是`普通模式`即会返回所有的验证不通过信息集合 1、当请求A时,id不传时,校验错误 默认、快速模式均返回 ``` {"code":3000,"msg":"校验错误","data":["id 用户ID不能为空"],"success":false} ``` 2、当请求B时,id、nickname有一个不传时,校验错误 2.1快速模式: ``` {"code":3000,"msg":"校验错误","data":["xxx不能为空"],"success":false} ``` 2.2默认模式 ``` {"code":3000,"msg":"校验错误","data":["id 用户ID不能为空","nickname 昵称不能为空"],"success":false} ``` 3、当请求C时,id、code、phone有一个不传时,校验错误 3.1快速模式: ``` {"code":3000,"msg":"校验错误","data":["xxx不能为空"],"success":false} ``` 3.2默认模式 ``` {"code":3000,"msg":"校验错误","data":["id 用户ID不能为空","code 验证码不能为空","phone 手机不能为空"],"success":false} ``` ### 3、自定义效验 在上述分组示例中,很明显手机号是11位数字类型,在内置注解中无法只用单个注解同时满足3个条件 > * 长度11位 > * 0-9数字组成 > * 手机号段 所以需要自定义处理,这里又可以分2种: > * 组合验证:通过内置的注解组合验证(如:验证手机号) VMobile.java ``` // 申明注解的作用位置 @Target({ANNOTATION_TYPE, FIELD, METHOD, PARAMETER}) // 运行时机 @Retention(RUNTIME) // 定义对应的校验器,自定义注解必须指定 @Constraint(validatedBy = {}) // 非空验证 @NotEmpty(message = "手机号不能为空") // 长度验证 @Length(min = 11, max = 11, message = "手机号长度错误") // 正则验证(第1为:1) + (第2位:3、4、5、6、7、8、9) + (第3-11位:0-9)+ (第11位:0-9数字结尾) @Pattern(regexp = "/^[1][3、4、5、6、7、8、9][0-9]{9}$/") public @interface VMobile { String message() default "手机号格式错误";// 错误提示信息默认值,可以使用el表达式。 Class<?>[] groups() default {};// 约束注解在验证时所属的组别 Class<? extends Payload>[] payload() default {};// 约束注解的有效负载 } ``` > * 自定义校验器:通过校验器来验证(如:验证JSON格式) VJson.java ``` // 申明注解的作用位置 @Target({ANNOTATION_TYPE, FIELD, METHOD, PARAMETER}) // 运行时机 @Retention(RUNTIME) // 定义对应的校验器,自定义注解必须指定 @Constraint(validatedBy = {VJsonRule.class}) // 非空验证 @NotEmpty(message = "json不能为空") public @interface VJson { String message() default "json格式错误";// 错误提示信息默认值,可以使用el表达式。 Class<?>[] groups() default {};// 约束注解在验证时所属的组别 Class<? extends Payload>[] payload() default {};// 约束注解的有效负载 } ``` VJsonRule.java ``` public class VJsonRule implements ConstraintValidator<VJson, String> { @Override public void initialize(VJson json) {} @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { try { if (StringUtils.isEmpty(s)) { return false; } // 判断是否是json(原理:利用fastjson中str-->jsonOject 如果报错则非不是,反则是) JSON.parseObject(s); return true; } catch (Exception e) { return false; } } } ``` 在VO中,则可以使用我们自定义的@VMobile、@VJson注解,无需重复设置message ``` @VMobile private String phone; @VJson private String json; ``` ### 4、对象校验 ![](https://img.kancloud.cn/8c/cf/8ccf4b4c6ea2668cbb4eadebf186b913_1010x345.png) 上述1.2.3都是接收数据时校验VO,那么在业务对象BO,持久对象PO这类怎么去校验? ``` @Slf4j @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = FastBootApplication.class) public class ValidatorHelperTest { @Autowired Validator validator; @Test public void verify1() { TestVo vo = new TestVo(); Set<ConstraintViolation<TestVo>> violations = validator.validate(vo, Default.class); List<String> message = ValidatorHelper.extractMessage(violations); log.info(JSON.toJSONString(R.succ(message))); } @Test public void verify2() { TestVo vo = new TestVo(); try { ValidatorHelper.validate(vo); } catch (ConstraintViolationException e) { // 打印 messgae List<String> message1 = ValidatorHelper.extractMessage(e); log.info(JSON.toJSONString(R.succ(message1))); // 打印property + messgae Map<String, String> message2 = ValidatorHelper.extractPropertyAndMessage(e); log.info(JSON.toJSONString(R.succ(message2))); // 打印property + messgae List<String> message3 = ValidatorHelper.extractPropertyAndMessageAsList(e); log.info(JSON.toJSONString(R.succ(message3))); } } } ``` 本框架中默认注入Validator,直接使用即可 方式1 ``` @Autowired Validator validator; ``` 方式2 ``` Validator validator = SpringHelper.getBean(Validator.class); ``` 封装方法 ``` public static void validate(Object object) throws ConstraintViolationException public static void validate(Object object, Class<?>... groups) throws ConstraintViolationException public static List<String> extractMessage(ConstraintViolationException e) public static List<String> extractMessage(Set<? extends ConstraintViolation> constraintViolations) public static Map<String, String> extractPropertyAndMessage(ConstraintViolationException e) public static Map<String, String> extractPropertyAndMessage(Set<? extends ConstraintViolation> constraintViolations) public static List<String> extractPropertyAndMessageAsList(ConstraintViolationException e) public static List<String> extractPropertyAndMessageAsList(Set<? extends ConstraintViolation> constraintViolations) public static List<String> extractPropertyAndMessageAsList(ConstraintViolationException e, String separator) public static List<String> extractPropertyAndMessageAsList(Set<? extends ConstraintViolation> constraintViolations, String separator) ``` 校验结果 ``` [FBOOT][ INFO][08-29 23:14:28]-->[main: 5648][verify1(ValidatorHelperTest.java:39)] | - {"code":0,"msg":"操作成功","data":["参数不能为空"],"success":true} [FBOOT][ INFO][08-29 23:14:28]-->[main: 5660][verify2(ValidatorHelperTest.java:50)] | - {"code":0,"msg":"操作成功","data":["参数不能为空"],"success":true} [FBOOT][ INFO][08-29 23:14:28]-->[main: 5662][verify2(ValidatorHelperTest.java:54)] | - {"code":0,"msg":"操作成功","data":{"id":"参数不能为空"},"success":true} [FBOOT][ INFO][08-29 23:14:28]-->[main: 5663][verify2(ValidatorHelperTest.java:58)] | - {"code":0,"msg":"操作成功","data":["id 参数不能为空"],"success":true} ``` 以上4点是在编写GOTV服务中最常用的效验方式。`Hibernate-Validator`远不仅如此,还有很多如校验方式... 当实际场景已无法满足或用起来不是那么优雅的时候,更多见[官方文档](http://hibernate.org/validator/documentation/)~