zoukankan      html  css  js  c++  java
  • 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=u8eabu4efdu8bc1u53f7u5fc5u987bu662fu7b26u5408u89c4u5219u768418u4f4d
    # 姓名长度必须大于2小于20
    model.user.NAME_SIZE_BETWEEN_2_AND_20=u59d3u540du957fu5ea6u5fc5u987bu5927u4e8e2u5c0fu4e8e20
    # 姓名不能为空
    model.user.NAME_NOT_BLANK=u59d3u540du4e0du80fdu4e3au7a7a
    # 年龄最小为1
    model.user.AGE_MIN_1=u5e74u9f84u6700u5c0fu4e3a1
    # 年龄最大为200
    model.user.AGE_MAX_200=u5e74u9f84u6700u5927u4e3a200
    # 年龄不能为空
    model.user.AGE_NOT_NULL=u5e74u9f84u4e0du80fdu4e3au7a7a
    # 身份证号不能为空
    model.user.ID_CARD_NOT_NULL=u8eabu4efdu8bc1u53f7u4e0du80fdu4e3au7a7a
    

    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!orghibernatevalidatorValidationMessages.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

  • 相关阅读:
    Markdown 图片与图床使用
    gitignore
    设置或更改Mac文件的默认打开程序
    Hive时间处理
    csv大文件处理方案-数据量超表格最大容纳行数解决方案
    js中的闭包之我理解
    ASP.NET MVC5+EF6+EasyUI 后台管理系统(73)-微信公众平台开发-消息管理
    关于23种设计模式的有趣见解
    一步一步写算法(之 算法总结)
    ajax跨域通信-博客园老牛大讲堂
  • 原文地址:https://www.cnblogs.com/col-smile/p/13289834.html
Copyright © 2011-2022 走看看