基础概念
将数据验证作为业务逻辑既有优点也有缺点,spring提供了一种验证数据的设计。
数据验证不应该绑定到web层,同时很容易本地化(多语言),同时支持插件式扩展。
考虑到这些问题,Spring提供了一个Validator契约,它既是基本的,而且在应用程序的每一层都非常可用。
数据绑定Data binding对于用户动态输入绑定到领域模型是非常一样的。Spring实现了DataBinder来支持该功能。Spring在validation包中提供了在数据验证中主要使用的Validator、DataBinder,同时不限制只在web层使用。
Spring实现的DataBinder、低级的BeanWrapper都是基于PropertyEditorSupport去解析、格式化属性值的。
Spring提供了core.convert实现了通用的类型转换功能,以及高级的format包实现了格式化ui字段。可以使用这些字段替换PropertyEditorSupport。
只要类路径上有JSR-303实现(例如Hibernate验证器),就会自动启用Bean验证1.1支持的方法验证功能。
目标类标识了Constraint Annotation注解的方法,需要通过在类上标注@Validated注解来激活。
@Service
@Validated
public class MyBean {
public Archive findByCodeAndAuthor(@Size(min = 8, max = 10) String code, Author author) {
return null;
}
}
当解析在Constraint Annotation message里的{parameters}时,可以通过MessageSource读取应用的message.properties 里的配置。
使用Spring的Validator接口进行验证
Spring有个Validator接口可以使用去验证对象。
Validator接口通过使用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");
}
}
}
- supports(Class):是否改Validator支持改Class的实例
- validate(Object, org.springframework.validation.Errors):验证对象,并将错误信息添加到Errors中
- ValidationUtils.rejectIfEmpty:验证name不能为empty。
对于复杂、多级的对象可以通过嵌套多个Validator来实现验证,同时复用Validator。
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();
}
}
}
- CustomerValidator有个Address属性,通过addressValidator来验证。
- 其他的属性通过
ValidationUtils.rejectIfEmptyOrWhitespace验证
Spring中Validation的使用和建议
SpringMVC对于RequestMapping方法有内置的验证。验证包括一些两个级别:
- 使用@Valid、@Validated验证
@ModelAttribute, @RequestBody, and @RequestPart。如果验证失败就会产生MethodArgumentNotValidException异常 - 使用Constraint注解,比如@NotNull、@Size等标注到参数上。这种情况验证失败产生
HandlerMethodValidationException异常。 应用需要同时处理MethodArgumentNotValidExceptionandHandlerMethodValidationException这两个异常。
为了充分使用SpringMVC内置的支持方法验证,建议不要在
Controller类上添加@Validated注解。
通过MethodArgumentNotValidException and HandlerMethodValidationException产生的异常信息可以通过MessageSource、locale、language 指定配置文件来实现定制化。
为了捕获异常信息,可以继承ResponseEntityExceptionHandler、使用@ExceptionHandler、@ControllerAdvice直接处理HandlerMethodValidationException,里面包含了一些ParameterValidationResult验证结果。
实战(参数校验、异常捕获、错误信息本地化)
引入依赖
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.4'
id 'io.spring.dependency-management' version '1.1.6'
}
group = 'cn.mj'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
定义一个测试Bean
对应name、age两个属性进行验证同时指定了验证失败时的错误信息key。
public class TestBean {
@NotBlank(message = "{TestBean.name}")
private String name;
@Min(value = 18, message = "{TestBean.age}")
private Integer age;
//...
}
定义一个测试Controller
@RestController
public class TestController {
@PostMapping("/test")
@Validated
public ResponseEntity<String> test(@Valid @RequestBody TestBean testBean, @RequestParam("id") @NotNull(message = "id不能为空") @Min(value = 1,message = "id必须大于1") Long id)
{
return ResponseEntity.ok("ok");
}
}
定义一个测试Service
@Service
@Validated
public class TestService {
@Validated(Create.class)
public void test(@Valid TestBean testBean){
System.out.println(testBean);
}
}
定义一个异常处理器
- MethodArgumentNotValidException
- HandlerMethodValidationException
- ConstraintViolationException 区别上面有说过。
@ControllerAdvice
public class ValidateExceptionHandler {
/**
* 验证基本参数
*/
@ResponseBody
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidException(MethodArgumentNotValidException e){
Object[] detailMessageArguments = e.getDetailMessageArguments();
List<String> messageList = Arrays.stream(detailMessageArguments).map(Object::toString).toList();
return ResponseEntity.ok().body(String.join( ",", messageList));
}
/**
* 验证对象
*/
@ResponseBody
@ExceptionHandler(HandlerMethodValidationException.class)
public ResponseEntity<?> handleValidException(HandlerMethodValidationException e){
Object[] detailMessageArguments = e.getDetailMessageArguments();
List<String> messageList = Arrays.stream(detailMessageArguments).map(Object::toString).toList();
return ResponseEntity.ok().body(String.join( ",", messageList));
}
/**
* Controller以内参数校验失败
*/
@ResponseBody
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<?> handleValidException(ConstraintViolationException e){
for (ConstraintViolation<?> constraintViolation : e.getConstraintViolations()) {
String message = constraintViolation.getMessage();
return ResponseEntity.ok().body(String.join( ",", message));
}
return ResponseEntity.ok().body("参数校验失败");
}
}
定义错误信息本地化配置文件
位置:resources: 18n/messages_zh_CN.properties
TestBean.name=name不能为空
TestBean.age=age不能为空
位置:resources: 18n/messages_en_US.properties
TestBean.name=name is not null
TestBean.age=age is not null
指定本地化配置文件地址
application.properties
spring.messages.basename=i18n/messages
请求
POST http://127.0.0.1:8080/test?id=2
Content-Type: application/json
Accept-Language: en-US
{
"name": "",
"age":6
}
====
2024-10-28 新增补充ConstraintViolationException处理。
- ConstraintViolationException在Web层后面进行处理