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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM