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
分组接口
附录