目录

Spring Bean Validation

Spring 通过设置基础设施和 Spring 自己的 Validator 合同的适配器支持 Java Bean Validation。应用程序可以全局启用一次 Bean 验证

通过 Spring 中 Validator 接口验证

org.springframework.validation.Validator 接口可以用来验证对象,使用 Errors 对象报告验证失败的信息。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
public interface Validator {

  boolean supports(Class<?> clazz);
  void validate(Object target, Errors errors);
}

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}
public class PersonValidator implements Validator {

    /**
     * This Validator validates only Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                "support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }

    /**
     * This Validator validates Customer instances, and any subclasses of Customer too
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

MessageCodesResolver 确定Errors接口寄存器的错误代码。默认情况下, DefaultMessageCodesResolver使用,它(例如)不仅使用您提供的代码注册消息,而且还注册包含您传递给拒绝方法的字段名称的消息。所以,如果你使用 拒绝一个字段rejectValue(“age”, “too.darn.old”),除了too.darn.old代码之外,Spring 还会注册too.darn.old.ageand too.darn.old.age.int

配置 Bean Validation Provider

Spring Framework 提供对 Java Bean Validation API 的支持。

Bean Validation 提供程序,例如 Hibernate Validator,预计会出现在类路径中并且会被自动检测到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

@Configuration
public class AppConfig {

    @Bean
    public LocalValidatorFactoryBean validator() {
        return new LocalValidatorFactoryBean();
    }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import javax.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

import org.springframework.validation.Validator;

@Service
public class MyService {

    @Autowired
    private Validator validator;
}

自定义约束

每个 bean 验证约束由两部分组成:

  • @Constraint声明约束及其可配置属性的注释。
  • javax.validation.ConstraintValidator 实现约束行为的接口的实现。
1
2
3
4
5
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy=MyConstraintValidator.class)
public @interface MyConstraint {
}

Spring-driven Method Validation

通过 bean 定义将 Bean Validation 1.1(以及作为自定义扩展,也由 Hibernate Validator 4.3)支持的方法验证功能集成到 Spring 上下文中 MethodValidationPostProcessor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@Configuration
public class BeanConfig {

  /**
   * 参数校验资源文件乱码
   *
   * <p>名字不能 messageSource 会替换 messages.properties
   *
   * @return
   */
  @Bean
  public MessageSource validationMessageSource() {
    ResourceBundleMessageSource source = new ResourceBundleMessageSource();
    // 读取配置文件的编码格式
    source.setDefaultEncoding(StandardCharsets.UTF_8.name());
    // 缓存时间,-1表示不过期
    source.setCacheMillis(-1);
    //  // 配置文件前缀名,设置为Messages,那你的配置文件必须以Messages.properties/Message_en.properties...
    source.setBasename("ValidationMessages");
    return source;
  }

  @Bean
  public Validator validator() {
    LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
    MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
    factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
    factoryBean.setValidationMessageSource(validationMessageSource());
    return factoryBean;
  }

  /**
   * 方法级参数验证
   *
   * @return 方法验证
   */
  @Bean
  public MethodValidationPostProcessor methodValidationPostProcessor() {
    MethodValidationPostProcessor methodValidationPostProcessor =
        new MethodValidationPostProcessor();
    methodValidationPostProcessor.setValidator(validator());
    return methodValidationPostProcessor;
  }
}

有了方法级别验证,我们就能够更加简单的在Java世界进行契约式设计了

没有 MethodValidationPostProcessor 之前我们可能这样验证:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public UserModel get(Integer uuid) {  
    //前置条件  
    Assert.notNull(uuid);  
    Assert.isTrue(uuid > 0, "uuid must lt 0");  
  
    //获取 User Model  
    UserModel user = new UserModel(); //此处应该从数据库获取  
  
    //后置条件  
    Assert.notNull(user);  
    return user;  
}  

前置条件和后置条件的书写是很烦人的工作。

有了MethodValidationPostProcessor之后我们可以这样验证:验证方法参数和和返回值

1
2
3
4
5
public @NotNull UserModel get2(@NotNull @Size(min = 1) Integer uuid) {  
    //获取 User Model  
    UserModel user = new UserModel(); //此处应该从数据库获取  
    return user;  
}  
  • 前置条件的验证:在方法的参数上通过Bean Validation注解进行实施;
  • 后置条件的验证:直接在返回值上通过Bean Validation注解进行实施。

JSR 和 Hibernate validator 的校验只能对 Object 的属性进行校验,不能对单个的参数进行校验,spring 在此基础上进行了扩展,添加了 MethodValidationPostProcessor 拦截器,可以实现对方法参数的校验。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RestController
@Validated
public class ValidateController {
}

    @RequestMapping(value = "/test", method = RequestMethod.GET)
    public String paramCheck(@Validated @Length(min = 10) @RequestParam String name) {
        System.out.println(name);
        return null;
    }

当方法上面的参数校验失败, Spring 框架就回抛出异常

1
2
3
4
5
6
7
8
{
  "timestamp": 1476108200558,
  "status": 500,
  "error": "Internal Server Error",
  "exception": "javax.validation.ConstraintViolationException",
  "message": "No message available",
  "path": "/test"
}

Configuring a DataBinder

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Foo target = new Foo();
DataBinder binder = new DataBinder(target);
binder.setValidator(new FooValidator());

// bind to the target object
binder.bind(propertyValues);

// validate the target object
binder.validate();

// get BindingResult that includes any validation errors
BindingResult results = binder.getBindingResult();

BeanValidationPostProcessor

BeanValidationPostProcessor 它就是个普通的BeanPostProcessor。它能够去校验Spring容器中的Bean,从而决定允不允许它初始化完成。

比如我们有些Bean某些字段是不允许为空的,比如数据的链接,用户名密码等等,这个时候用上它处理就非常的优雅和高级了~

若校验不通过,在违反约束的情况下就会抛出异常,阻止容器的正常启动~

1
2
3
4
5
6
7
8
public class UserModel {  
    @NotNull(message = "user.username.null")  
    @Pattern(regexp = "[a-zA-Z0-9_]{5,10}", message = "user.username.illegal")  
    private String username;  
    @Size(min = 5, max=10, message = "password.length.illegal")  
    private String password;  
    //省略setter/getter  
}  

spring-config-bean-validator.xml

1
2
3
4
5
6
<!--注册Bean验证后处理器-->  
<bean class="org.springframework.validation.beanvalidation.BeanValidationPostProcessor"/>
<bean id="user" class="com.sishuok.validator.UserModel">  
    <property name="username" value="@"/>  
    <property name="password" value="#"/>  
</bean> 

测试用例

1
2
3
4
5
6
7
8
9
@RunWith(value = SpringJUnit4ClassRunner.class)  
@ContextConfiguration(value = {"classpath:spring-config-bean-validator.xml"})  
public class BeanValidatorTest {  
    @Autowired  
    UserModel user;  
    @Test  
    public void test() {  
    }  
}  

运行测试后,容器启动失败并将看到如下异常:

java.lang.IllegalStateException: Failed to load ApplicationContext  
……  
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'user' defined in class path resource [spring-config-bean-validator.xml]: Initialization of bean failed; nested exception is org.springframework.beans.factory.BeanInitializationException: Bean state is invalid: password - password.length.illegal; username - user.username.illegal  
……  
Caused by: org.springframework.beans.factory.BeanInitializationException: Bean state is invalid: password - password.length.illegal; username - user.username.illegal 

校验信息的国际化显示

配置文件 springmvc-servlet.xml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
<!-- 指定自己定义的validator -->
<mvc:annotation-driven validator="validator"/> 
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">    
    <property name="basename" value="classpath:errors" />
    <property name="defaultEncoding" value="UTF-8"/>
</bean>
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
    <!-- 如果不加默认到 使用classpath下的 ValidationMessages.properties -->    
    <property name="validationMessageSource" ref="messageSource"/>
</bean>

两个国际化文件errors_zh_CN.properties和errors_en_US.properties

error.age = Max age is 150 years old.
error.age = \u5e74\u9f84\u6700\u5927\u662f\u0031\u0032\u0030\u5c81

validation.min.age=不能小于{value}
1
2
3
@Max(value = 120, message = "{error.age}")
@Min(value= 18, message = "{age}{validation.min.age}")
private int age;

message中使用EL表达式

1
2
3
4
5
6
<dependency>
  <groupId>javax.el</groupId>
  <artifactId>javax.el-api</artifactId>
  <version>2.2.4</version>
  <scope>provided</scope>
</dependency>

Controller 层使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
//非实体类参数可以直接使用注解
@GetMapping("/check")
@ResponseBody
public String check(@Min(0,message = "kpId必须大于等于0") @RequestParam int kpId,@RequestParam int level) {
        return "ok";
}
    //实体类注解校验使用@Valid
    @PostMapping("/")
    public String checkPersonInfo(@Valid PersonForm personForm, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "form";
        }
        return "redirect:/results";
 }

Spring提供的BindingResult是错误结果的一个封装,我们可以在web页面中通过这个对象拿到详细的错误信息,

1
<td th:if="${#fields.hasErrors('age')}" th:errors="*{age}">Age Error</td>

一个@Valid的参数后必须紧挨着一个BindingResult 参数,否则spring会在校验不通过时直接抛出异常

Spring Boot

1
2
3
4
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

表单验证 @Validated 的 message 国际化资源文件默认必须放在 resources/ValidationMessages.properties 中。

自定义路径 resources/i18n/validation/message.properties

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Configuration
public class ValidationConfig {
    @Bean
    public Validator getValidator() {
        Validator validator = Validation.byDefaultProvider().
        configure().
        messageInterpolator(new ResourceBundleMessageInterpolator(new PlatformResourceBundleLocator("i18n/validation/message"))).
        buildValidatorFactory().getValidator();
        return validator;
    }
}

Validator 国际化配置文件说明

  • 国际化配置文件必须放在classpath的根目录下,即src/java/resources的根目录下。
  • 国际化配置文件必须以 ValidationMessages 开头,比如ValidationMessages.properties 或者 ValidationMessages_en.properties。
  • 有些判断逻辑是很难使用Validator的注解来实现,比如存在name,那么返回“该name已经存在了”。利用result.rejectValue(“name”, “misFormat”, “该name已经存在了!”);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Slf4j
@Component
@RestControllerAdvice
public class GlobalExceptionHandler {
  /**
   * 参数绑定异常
   *
   * @param e 异常
   * @return 异常结果
   */
  @ExceptionHandler(value = BindException.class)
  @ResponseBody
  public Result<String> handleBindException(BindException e) {
    log.error("参数绑定校验异常", e);

    return wrapperBindingResult(e.getBindingResult());
  }

  /**
   * 参数校验异常,将校验失败的所有异常组合成一条错误信息
   *
   * @param e 异常
   * @return 异常结果
   */
  @ExceptionHandler(value = MethodArgumentNotValidException.class)
  @ResponseBody
  public Result<String> handleValidException(MethodArgumentNotValidException e) {
    log.error("参数绑定校验异常", e);

    return wrapperBindingResult(e.getBindingResult());
  }

  /**
   * 包装绑定异常结果
   *
   * @param bindingResult 绑定结果
   * @return 异常结果
   */
  private Result<String> wrapperBindingResult(BindingResult bindingResult) {
    StringBuilder msg = new StringBuilder();

    for (ObjectError error : bindingResult.getAllErrors()) {
      msg.append(", ");
      if (error instanceof FieldError) {
        msg.append(((FieldError) error).getField()).append(": ");
      }
      msg.append(error.getDefaultMessage() == null ? "" : error.getDefaultMessage());
    }

    return Result.errorData(ResultCode.VALID_ERROR, msg.substring(2));
  }
}

自定义验证注解

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@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 constraintAnnotation) {}

  @Override
  public boolean isValid(Object value, ConstraintValidatorContext context) {
    if (value == null) {
      return true;
    }
    return cn.hutool.core.util.IdcardUtil.isValidCard(String.valueOf(value));
  }
}

总结

  • 级联约束 对象类型字段前加 @Valid
  • 对 Controller 中方法多个参数校验可以在类上加 @Validated 注解
  • 全局异常处理 MethodArgumentNotValidException
  • 建议分组接口继承 Default 分组接口

附录