从0到1认识Spring Validation

479 阅读4分钟

基础概念

将数据验证作为业务逻辑既有优点也有缺点,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");
        }
    }
}
  1. supports(Class):是否改Validator支持改Class的实例
  2. validate(Object, org.springframework.validation.Errors):验证对象,并将错误信息添加到Errors中
  3. 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();
       }
    }
}
  1. CustomerValidator有个Address属性,通过addressValidator来验证。
  2. 其他的属性通过ValidationUtils.rejectIfEmptyOrWhitespace验证

Spring中Validation的使用和建议

SpringMVC对于RequestMapping方法有内置的验证。验证包括一些两个级别:

  1. 使用@Valid、@Validated验证@ModelAttribute, @RequestBody, and @RequestPart 。如果验证失败就会产生MethodArgumentNotValidException异常
  2. 使用Constraint注解,比如@NotNull、@Size等标注到参数上。这种情况验证失败产生HandlerMethodValidationException异常。 应用需要同时处理MethodArgumentNotValidException and HandlerMethodValidationException这两个异常。

为了充分使用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);
    }
}

定义一个异常处理器

  1. MethodArgumentNotValidException
  2. HandlerMethodValidationException
  3. 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处理。

  1. ConstraintViolationException在Web层后面进行处理