https://www.jianshu.com/p/2432d0f51c0e

前言

本文Spring版本为 SpringBoot-2.0.7,所有源码相关类、方法、代码行都以此版本为基础。 代码行数: 使用 IDEA 的同学通过Maven Projects -> Donwload Sources and Documentation下载源码及注释文档,保证行数的准确。 非常欢迎您指正在文章中出现的错误,包括但不限于 语句错误、描述错误、示例错误、代码理解错误。

参数校验是代码开发中必不可少的一环,一个方法中参数校验套了一个又一个 if-else,繁琐的操作让广大程序员诟病。
本文我们就讲一下 SpringBoot 结合 Hibernate-Validtor 校验参数、简化工作。

开始

spring-boot-starter-web 已经默认整合、提供了 Hibernate-Validator 的功能,只待我们去使用。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-web</artifactId>
  4. <version>2.0.7.RELEASE</version>
  5. </dependency>

创建一个实体类,并添加校验注解。

本小节只做简单的使用演示。 常用注解列表、注解说明、注解用法,以及·自定义校验注解·的教程。JSR 303 - Bean Validation 介绍及最佳实践

  1. public class Student{
  2. @NotNull
  3. private String name;
  4. @NotNull
  5. private String sex;
  6. @Min(0)
  7. @Max(150)
  8. private int age;
  9. ...get,set...
  10. }

接着编写 Controller 代码。

  1. // @RestController
  2. // DemoController
  3. @GetMapping("/student")
  4. public String validator(@Validated Student student, BindingResult result) {
  5. if (result.hasErrors()) {
  6. return result.getFieldError().getDefaultMessage();
  7. }
  8. return "ok";
  9. }

启动程序后访问http://{host:prot}/student,将会返回:

  1. must not be null

访问http://{host:prot}/student?name=zhangsan&sex=Male&age=22,将会返回:

  1. ok

到这,本期的教程结束…是不可能的。

进阶

上面的教程还太简单,很多事情都很朦胧。

  1. 书写有没有什么规则?
  2. 我怎么知道‘must not be null’是指哪个参数?跟没提示一样。
  3. 每个 Controller 方法都要判断 BindingResult 还是好麻烦!我懒得写!
  4. 校验规则太少了,能不能自己写规则?
  5. 我想手动校验怎么办?

    书写规则

    @Validated 和 @Valid 的异同
    @Validated 是 Spring 实现的JSR-303的变体 @Valid ,支持验证组的规范。 设计用于方便使用Spring的JSR-303支持,但不支持JSR-303特定。
    @Valid JSR-303标准实现的校验注解。
注解 范围 嵌套 校验组
@Validated 可以标记类、方法、方法参数,不能用在成员属性(字段)上 不支持 支持
@Valid 可以标记方法、构造函数、方法参数和成员属性(字段)上 支持 不支持

两者都可以用在方法入参上,但都无法单独提供嵌套验证功能,都能配合嵌套验证注解@Valid进行嵌套验证。
嵌套验证示例:

  1. public class ClassRoom{
  2. @NotNull
  3. String name;
  4. @Valid // 嵌套校验,校验参数内部的属性
  5. @NotNull
  6. Student student;
  7. }
  1. @GetMapping("/room") // 此处可使用 @Valid 或 @Validated, 将会进行嵌套校验
  2. public String validator(@Validated ClassRoom classRoom, BindingResult result) {
  3. if (result.hasErrors()) {
  4. return result.getFieldError().getDefaultMessage();
  5. }
  6. return "ok";
  7. }

参考:@Validated和@Valid区别—-CSDN:花郎徒结

BindingResult 的使用

BindingResult必须跟在被校验参数之后,若被校验参数之后没有BindingResult对象,将会抛出BindException

  1. @GetMapping("/room")
  2. public String validator(@Validated ClassRoom classRoom, BindingResult result) {
  3. if (result.hasErrors()) {
  4. return result.getFieldError().getDefaultMessage();
  5. }
  6. return "ok";
  7. }

不要使用 BindingResult 接收,String等简单对象的错误信息。简单对象校验失败,会抛出 ConstraintViolationException
主要就是接不着,你要写也算是没关系…

  1. // ❌ 错误用法,也没有特别的错,只是 result 是接不到值。
  2. @GetMapping("/room")
  3. @Validated // 启用校验
  4. public String validator(@NotNull String name, BindingResult result) {
  5. if (result.hasErrors()) {
  6. return result.getFieldError().getDefaultMessage();
  7. }
  8. return "ok";
  9. }

修改校验失败的提示信息

可以通过各个校验注解的message属性设置更友好的提示信息。

  1. public class ClassRoom{
  2. @NotNull(message = "Classroom name must not be null")
  3. String name;
  4. @Valid
  5. @NotNull
  6. Student student;
  7. }
  1. @GetMapping("/room")
  2. @Validated
  3. public String validator(ClassRoom classRoom, BindingResult result, @NotNull(message = "姓名不能为空") String name) {
  4. if (result.hasErrors()) {
  5. return result.getFieldError().getDefaultMessage();
  6. }
  7. return "ok";
  8. }

message属性配置国际化的消息也可以的,message中填写国际化消息的code,在抛出异常时根据code处理一下就好了。

  1. @GetMapping("/room")
  2. @Validated
  3. public String validator(@NotNull(message = "demo.message.notnull") String name) {
  4. if (result.hasErrors()) {
  5. return result.getFieldError().getDefaultMessage();
  6. }
  7. return "ok";
  8. }
  1. // message_zh_CN.properties
  2. demo.message.notnull=xxx消息不能为空
  3. // message_en_US.properties
  4. demo.message.notnull=xxx message must no be null

省略 Controller 中的校验判断

可以利用参数校验失败后抛出异常这点,配置·统一异常拦截·,进行异常统一的处理,合理的将错误信息返回给前端。
抛砖(仅做示例):

  1. // @RestControllerAdvice
  2. /* 数据校验处理 */
  3. @ExceptionHandler({BindException.class, ConstraintViolationException.class})
  4. public String validatorExceptionHandler(Exception e) {
  5. String msg = e instanceof BindException ? msgConvertor(((BindException) e).getBindingResult())
  6. : msgConvertor(((ConstraintViolationException) e).getConstraintViolations());
  7. return msg;
  8. }
  9. /**
  10. * 校验消息转换拼接
  11. *
  12. * @param bindingResult
  13. * @return
  14. */
  15. public static String msgConvertor(BindingResult bindingResult) {
  16. List<FieldError> fieldErrors = bindingResult.getFieldErrors();
  17. StringBuilder sb = new StringBuilder();
  18. fieldErrors.forEach(fieldError -> sb.append(fieldError.getDefaultMessage()).append(","));
  19. return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
  20. }
  21. private String msgConvertor(Set<ConstraintViolation<?>> constraintViolations) {
  22. StringBuilder sb = new StringBuilder();
  23. constraintViolations.forEach(violation -> sb.append(violation.getMessage()).append(","));
  24. return sb.deleteCharAt(sb.length() - 1).toString().toLowerCase();
  25. }

注:getMessagegetDefaultMessage 都是直接获取注解上message属性的值,

扩展校验注解、校验规则

常用注解列表、注解说明、注解用法,以及·自定义校验注解·的教程。JSR 303 - Bean Validation 介绍及最佳实践

手动校验

若没有手动配置Validator对象,自然需要从 Spring 容器中获取校验器对象,注入使用。
此处给出一个手动校验的工具类,供大家参考。(lay了…写的自闭,如果对代码有疑问请联系我..持续更新)
代码中提到的与 Spring 集成,主要是对代码返回值的统一。(不支持普通对象…)
若都以注解的message属性来获取提示消息,可以删除 Spring 相关的代码。
若不以message属性作为消息,那么可以从bindingResult中获取字段、类、注解信息,拼装成消息码。
抛砖:

  1. // config
  2. // @Configuration
  3. @Bean
  4. public Validator validator() {
  5. return ValidatorUtils.getValidator();
  6. }
  1. import org.hibernate.validator.HibernateValidator;
  2. import org.springframework.util.ClassUtils;
  3. import org.springframework.validation.BindException;
  4. import org.springframework.validation.DataBinder;
  5. import org.springframework.validation.SmartValidator;
  6. import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
  7. import javax.validation.ConstraintViolation;
  8. import javax.validation.ConstraintViolationException;
  9. import javax.validation.Validation;
  10. import javax.validation.Validator;
  11. import java.util.Set;
  12. /**
  13. * hibernate-validator校验工具类
  14. */
  15. public class ValidatorUtils {
  16. private static Validator validator;
  17. private static SmartValidator validatorAdapter;
  18. static {
  19. // 快速返回模式
  20. validator = Validation.byProvider(HibernateValidator.class)
  21. .configure()
  22. .failFast(true)
  23. .buildValidatorFactory()
  24. .getValidator();
  25. }
  26. public static Validator getValidator() {
  27. return validator;
  28. }
  29. private static SmartValidator getValidatorAdapter(Validator validator) {
  30. if (validatorAdapter == null) {
  31. validatorAdapter = new SpringValidatorAdapter(validator);
  32. }
  33. return validatorAdapter;
  34. }
  35. /**
  36. * 校验参数,用于普通参数校验 [未测试!]
  37. *
  38. * @param
  39. */
  40. public static void validateParams(Object... params) {
  41. Set<ConstraintViolation<Object>> constraintViolationSet = validator.validate(params);
  42. if (!constraintViolationSet.isEmpty()) {
  43. throw new ConstraintViolationException(constraintViolationSet);
  44. }
  45. }
  46. /**
  47. * 校验对象
  48. *
  49. * @param object
  50. * @param groups
  51. * @param <T>
  52. */
  53. public static <T> void validate(T object, Class<?>... groups) {
  54. Set<ConstraintViolation<T>> constraintViolationSet = validator.validate(object, groups);
  55. if (!constraintViolationSet.isEmpty()) {
  56. throw new ConstraintViolationException(constraintViolationSet);
  57. }
  58. }
  59. /**
  60. * 校验对象
  61. * 使用与 Spring 集成的校验方式。
  62. *
  63. * @param object 待校验对象
  64. * @param groups 待校验的组
  65. * @throws BindException
  66. */
  67. public static <T> void validateBySpring(T object, Class<?>... groups)
  68. throws BindException {
  69. DataBinder dataBinder = getBinder(object);
  70. dataBinder.validate((Object[]) groups);
  71. if (dataBinder.getBindingResult().hasErrors()) {
  72. throw new BindException(dataBinder.getBindingResult());
  73. }
  74. }
  75. private static <T> DataBinder getBinder(T object) {
  76. DataBinder dataBinder = new DataBinder(object, ClassUtils.getShortName(object.getClass()));
  77. dataBinder.setValidator(getValidatorAdapter(validator));
  78. return dataBinder;
  79. }
  80. }

源码经验宝宝[拓展]

为什么 BindingResult 接收不到简单对象的校验信息?

跟进 Spring MVC 源码,发现:SpringMVC 在进行方法参数的注入(将 Http请求参数封装成方法所需的参数)时,不同的对象使用不同的解析器注入对象。
听着好像没什么关系。但其实就是,注入实体对象时使用ModelAttributeMethodProcessor而注入 String 对象使用AbstractNamedValueMethodArgumentResolver。而正是这个差异导致了BindingResult无法接受到简单对象(简单的入参参数类型)的校验信息。
啊?你问我什么是简单对象?emm…
不同的解析器支持不同类型的对象(不同的参数类型),需要看各解析器实现的supportsParameter()方法,标题提到的简单对象,意思是ModelAttributeMethodProcessor不支持的所有对象。
获取参数注入解析器的源码位于HandlerMethodArgumentResolverComposite#resolveArgument():120:

  1. // HandlerMethodArgumentResolverComposite.class
  2. public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  3. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  4. // 获取 parameter 参数的解析器
  5. HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  6. // 调用解析器获取参数
  7. return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  8. }
  9. // 获取 parameter 参数的解析器
  10. private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
  11. // 从缓存中获取参数对应的解析器
  12. HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
  13. for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
  14. // 解析器是否支持该参数类型
  15. if (methodArgumentResolver.supportsParameter(parameter)) {
  16. result = methodArgumentResolver;
  17. this.argumentResolverCache.put(parameter, result);
  18. break;
  19. }
  20. }
  21. return result;
  22. }

注入 String 参数时,在AbstractNamedValueMethodArgumentResolver#resolveArgument()中,不会抛出BindException/ConstraintViolationException异常、也不会将 BindingResult 传入到方法中。
注入对象时在ModelAttributeMethodProcessor#resolveArgument():154 行的 validateIfApplicable(binder, parameter)语句,进行了参数校验,校验不通过并且实体对象后不存在BindingResult对象,则会在this#resolveArgument():156抛出BindException

  1. public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
  2. NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  3. // bean 参数绑定和校验
  4. WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
  5. // 参数校验
  6. validateIfApplicable(binder, parameter);
  7. // 校验结果包含错误,并且该对象后不存在 BindingResult 对象,就抛出异常
  8. if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
  9. throw new BindException(binder.getBindingResult());
  10. }
  11. // 在对象后注入 BindingResult 对象
  12. Map<String, Object> bindingResultModel = bindingResult.getModel();
  13. mavContainer.removeAttributes(bindingResultModel);
  14. mavContainer.addAllAttributes(bindingResultModel);
  15. }

在哪里抛出ConstraintViolationException

可能有同学发现了,简单对象注入后并没有抛出异常,那这个参数在哪里被校验呢?
但是在 InvocableHandlerMethod#invokeForRequest():136 的 doInoke(args) 方法里,调用了 Mehtod.invoke() 调用真实的方法。到这里好像完了?!还是没有校验?那咋办,玄学啊!
当然,肯定不是玄学。这个方法的确是直接被调用了,但调用过程被拦截了。被方法级的拦截器拦住了。
拦截对象是 CglibAopProxy$CglibMethodInvocation 它还继承了ReflectiveMethodInvocation 这又是个啥呢,Spring 说它是个实现了 Spring AOP 方法调用接口的基类,可以拓展方法拦截的更高级的功能实现。
ReflectiveMethodInvocation#process()方法的最后一行:

  1. return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);

这里的 Methodnterceptor 接口的真身是 MethodValidationInterceptor:

  1. // MethodValidationInterceptor.class
  2. public Object invoke(MethodInvocation invocation) throws Throwable {
  3. ExecutableValidator execVal = this.validator.forExecutables();
  4. // 校验参数
  5. try {
  6. result = execVal.validateParameters(
  7. invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
  8. }
  9. catch (IllegalArgumentException ex) {
  10. // 解决参数错误异常、再次校验
  11. methodToValidate = BridgeMethodResolver.findBridgedMethod(
  12. ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
  13. result = execVal.validateParameters(
  14. invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
  15. }
  16. if (!result.isEmpty()) {
  17. throw new ConstraintViolationException(result);
  18. }
  19. // 执行结果
  20. Object returnValue = invocation.proceed();
  21. // 校验返回值
  22. result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
  23. if (!result.isEmpty()) {
  24. throw new ConstraintViolationException(result);
  25. }
  26. return returnValue;
  27. }

over.
本文到此结束。