4.SpringBoot学习(四)——Spring Boot Validation校验及原理

1.简介

1.1 概述

The method validation feature supported by Bean Validation 1.1 is automatically enabled as long as a JSR-303 implementation (such as Hibernate validator) is on the classpath. This lets bean methods be annotated with javax.validation constraints on their parameters and/or on their return value. Target classes with such annotated methods need to be annotated with the @Validated annotation at the type level for their methods to be searched for inline constraint annotations.

只要 JSR-303 的实现(例如Hibernate验证器)在 classpath下,就会自动启用 Bean Validation 1.1 支持的方法验证功能。这使 bean 方法的参数和/或返回值可以使用 javax.validation 注解进行约束。具有此类注释方法的目标类需要在类型级别使用@Validated注释进行注释,以便在其方法中搜索内联约束注释。

2.环境

  1. JDK 1.8.0_201
  2. Spring Boot 2.2.0.RELEASE
  3. 构建工具(apache maven 3.6.3)
  4. 开发工具(IntelliJ IDEA )
  5. 数据库:h2

3.代码

3.1 功能说明

用户 User 类里面有 id、name、age、idCard 等字段,这些字段在处理的时候通过注解进行校验;其中 name、age 字段校验使用的是 spring boot 依赖的组件中提供的注解;而 idCard 使用自定义注解 @IdCard;这些注解都支持国际化,最终通过 jpa 保存到 h2 数据库中。

UserCommand 用来预置几条数据。

3.2 代码结构

image-20200712172518494

3.3 maven 依赖

<dependencies>
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

3.4 配置文件

application.properties

# 开启h2数据库
spring.h2.console.enabled=true

# 配置h2数据库
spring.datasource.url=jdbc:h2:mem:user
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sad
spring.datasource.password=sae

# 是否显示sql语句
spring.jpa.show-sql=true
hibernate.dialect=org.hibernate.dialect.H2Dialect
hibernate.hbm2ddl.auto=create

ValidationMessages.properties

com.soulballad.usage.model.validation.id.card.message=the id card length must be 18 and matches rule
model.user.NAME_SIZE_BETWEEN_2_AND_20=the length of name must be greater than 2 and less than 20
model.user.NAME_NOT_BLANK=name cannot be blank
model.user.AGE_MIN_1=the minimum of age is 1
model.user.AGE_MAX_200=the maximum of age is 200
model.user.AGE_NOT_NULL=age cannot be null
model.user.ID_CARD_NOT_NULL=id card cannot be null

ValidationMessages_zh_CN.properties

# 身份证号必须是符合规则的18位
com.soulballad.usage.model.validation.id.card.message=\u8eab\u4efd\u8bc1\u53f7\u5fc5\u987b\u662f\u7b26\u5408\u89c4\u5219\u768418\u4f4d
# 姓名长度必须大于2小于20
model.user.NAME_SIZE_BETWEEN_2_AND_20=\u59d3\u540d\u957f\u5ea6\u5fc5\u987b\u5927\u4e8e2\u5c0f\u4e8e20
# 姓名不能为空
model.user.NAME_NOT_BLANK=\u59d3\u540d\u4e0d\u80fd\u4e3a\u7a7a
# 年龄最小为1
model.user.AGE_MIN_1=\u5e74\u9f84\u6700\u5c0f\u4e3a1
# 年龄最大为200
model.user.AGE_MAX_200=\u5e74\u9f84\u6700\u5927\u4e3a200
# 年龄不能为空
model.user.AGE_NOT_NULL=\u5e74\u9f84\u4e0d\u80fd\u4e3a\u7a7a
# 身份证号不能为空
model.user.ID_CARD_NOT_NULL=\u8eab\u4efd\u8bc1\u53f7\u4e0d\u80fd\u4e3a\u7a7a

3.5 java代码

User.java

@Entity
@JsonIgnoreProperties(value = { "hibernateLazyInitializer", "handler" })
public class User implements Serializable {

    @Id
    @GeneratedValue
    private Long id;

    @Size(min = 2, max = 20, message = "{model.user.NAME_SIZE_BETWEEN_2_AND_20}")
    @NotBlank(message = "{model.user.NAME_NOT_BLANK}")
    private String name;

    @Min(value = 1, message = "{model.user.AGE_MIN_1}")
    @Max(value = 200, message = "{model.user.AGE_MAX_200}")
    @NotNull(message = "{model.user.AGE_NOT_NULL}")
    private Integer age;

    @IdCard
    @NotNull(message = "{model.user.ID_CARD_NOT_NULL}")
    private String idCard;

    // get&set&constructors&toString
}

UserRepository.java

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
}

UserServiceImpl.java

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public List<User> selectAll() {
        return userRepository.findAll();
    }

    @Override
    public User getUserById(Long id) {
        return userRepository.getOne(id);
    }

    @Override
    public User add(User user) {
        return userRepository.save(user);
    }

    @Override
    public User update(User user) {
        return userRepository.save(user);
    }

    @Override
    public User delete(Long id) {
        User user = getUserById(id);
        userRepository.deleteById(id);
        return user;
    }
}

IdCard.java

/**
 * @apiNote : 自定义注解校验 {@link com.soulballad.usage.springboot.model.User} 中的idCard字段该注解中参数和 {@link NotNull} 中成员一致,不过 {@link NotNull} 中通过 {@link Repeatable} 声明了它是可复用的,
 *  并通过 {@link Constraint} 注解声明注解的功能实现类
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IdCardValidator.class})
public @interface IdCard {

    // ValidationMessages.properties 扩展自
    // org.hibernate.validator.hibernate-validator.6.0.19.Final.hibernate-validator-6.0.19.Final.jar!\org\hibernate\validator\ValidationMessages.properties
    String message() default "{com.soulballad.usage.model.validation.id.card.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

IdCardValidator.java

/**
 * @apiNote : IdCard校验:注解{@link IdCard}的校验功能实现,需要实现{@link ConstraintValidator}接口, 泛型中两个参数分别为 {@link IdCard} 和 @IdCard
 *          修饰的字段对应类型
 */
public class IdCardValidator implements ConstraintValidator<IdCard, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        // 校验身份证号:正规身份证号 18=2(省)+2(市)+2(区/县)+8(出生日期)+2(顺序码)+1(性别)+1(校验码)
        // 这里使用正则简单校验一下
        if (value.length() != 18) {
            return false;
        }

        // 身份证号正则表达式
        String regex = "^[1-9]\\d{5}(18|19|20)\\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\\d{3}[0-9Xx]$";

        return Pattern.matches(regex, value);
    }

    @Override
    public void initialize(IdCard constraintAnnotation) {

    }
}

UserController.java

@Controller
@RequestMapping(value = "/user")
public class UserController {

    @Autowired
    private UserService userService;

    @ResponseBody
    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public List<User> list() {
        return userService.selectAll();
    }

    @ResponseBody
    @RequestMapping(value = "/add", method = RequestMethod.POST)
    public User add(@Valid @RequestBody User user) {
        return userService.add(user);
    }

    @ResponseBody
    @RequestMapping(value = "/get/{id}", method = RequestMethod.GET)
    public User get(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    @ResponseBody
    @RequestMapping(value = "/delete/{id}", method = RequestMethod.DELETE)
    public User delete(@PathVariable Long id) {
        return userService.delete(id);
    }

    @ResponseBody
    @RequestMapping(value = "/update", method = RequestMethod.PUT)
    public User update(@Valid @RequestBody User user) {
        return userService.update(user);
    }
}

UserCommand.java

@Component
public class UserCommand implements CommandLineRunner {

    @Autowired
    private UserRepository userRepository;

    @Override
    public void run(String... args) throws Exception {

        // 身份证号由 http://sfz.uzuzuz.com/ 在线生成
        User user1 = new User("zhangsan", 23, "110101200303072399");
        User user2 = new User("lisi", 34, "110113198708074275");
        User user3 = new User("wangwu", 45, "110113197308182272");

        userRepository.saveAll(Arrays.asList(user1, user2, user3));
        userRepository.deleteById(3L);
    }
}

3.6 git 地址

spring-boot/spring-boot-04-bean-validate

4.结果

启动 SpringBoot04BeanValidateApplication.main 方法,在 spring-boot-04-bean-validate.http 访问下列地址,观察输出信息是否符合预期。

### GET /user/list
GET http://localhost:8080/user/list
Accept: application/json

image-20200712175001915

### GET /user/get/{id}
GET http://localhost:8080/user/get/1
Accept: application/json

image-20200712175029788

### POST /user/add success
POST http://localhost:8080/user/add
Content-Type: application/json
Accept: */*
Cache-Control: no-cache

{
  "name": "zhaoliu",
  "age": 43,
  "idCard": "110101200303072399"
}

image-20200712175147586

### POST /user/add idCard&name&age illegal
POST http://localhost:8080/user/add
Content-Type: application/json
Accept: */*
# Accept-Language: en_US 使用此配置可选择中、英文错误提示

{
  "name": "s",
  "age": 243,
  "idCard": "1101003072399"
}

image-20200712182715264

### PUT /user/update success
PUT http://localhost:8080/user/update
Content-Type: application/json
Accept: */*

{
  "id": 2,
  "name": "sunqi",
  "age": 43,
  "idCard": "110101200303072399"
}

image-20200712182912860

### DELETE /user/delete/{id} success
DELETE http://localhost:8080/user/delete/1
Content-Type: application/json
Accept: */*

image-20200712183003589

5.源码分析

5.1 注解校验如何生效的?

在 UserController#add 方法上有使用 @Valid 注解,标明这个方法需要校验,同时也可以使用 @Validated 注解标明要校验的位置。那么 @Valid 是如何生效的呢?

SpringBoot学习(三)——WebMVC及其工作原理 中,有跟踪 Spring MVC 的运行原理,@Valid 的注解校验就在

RequestMappingHandlerAdapter#invokeHandlerMethod 方法中

image-20200712204316361

在 ConstraintTree#validateSingleConstraint 中使用具体的 Validator 对参数进行校验

protected final <T, V> Set<ConstraintViolation<T>> validateSingleConstraint(ValidationContext<T> executionContext, ValueContext<?, ?> valueContext, ConstraintValidatorContextImpl constraintValidatorContext, ConstraintValidator<A, V> validator) {
    boolean isValid;
    try {
        V validatedValue = valueContext.getCurrentValidatedValue();
        isValid = validator.isValid(validatedValue, constraintValidatorContext);
    } catch (RuntimeException var7) {
        if (var7 instanceof ConstraintDeclarationException) {
            throw var7;
        }

        throw LOG.getExceptionDuringIsValidCallException(var7);
    }

    return !isValid ? executionContext.createConstraintViolations(valueContext, constraintValidatorContext) : Collections.emptySet();
}

image-20200712204519662

posted @ 2020-07-12 20:56  Soulballad  阅读(1551)  评论(0编辑  收藏  举报