- 2.1 空和非空检查
- 2.2 数值检查
- 2.3 Boolean 值检查
- 2.4 长度检查
- 2.5 日期检查
- 2.6 其它检查
- 2.7 Hibernate Validator 附加的约束注解
- 2.8 @Valid 和 @Validated
一、概述
当我们想提供可靠的 API 接口,对参数的校验,以保证最终数据入库的正确性,是 必不可少 的活。比如下图就是 我们一个项目里 新增一个菜单校验 参数的函数,写了一大堆的 if else 进行校验,非常的不优雅,比起枯燥的CRUD来说,参数校验更是枯燥。
这只是一个创建菜单的校验,只需要判断菜单,菜单url 以及菜单的父类id是否为空,上级菜单是否挂载正确,这样已经消耗掉了30,40行代码了,更不要说,管理后台创建商品这种参数贼多的接口。估计要写几百行校验代码了。
/** * 验证参数是否正确 */ private void verifyForm(SysMenuEntity menu){ if(StringUtils.isBlank(menu.getName())){ throw new RRException("菜单名称不能为空"); } if(menu.getParentId() == null){ throw new RRException("上级菜单不能为空"); } //菜单 if(menu.getType() == Constant.MenuType.MENU.getValue()){ if(StringUtils.isBlank(menu.getUrl())){ throw new RRException("菜单URL不能为空"); } } //上级菜单类型 int parentType = Constant.MenuType.CATALOG.getValue(); if(menu.getParentId() != 0){ SysMenuEntity parentMenu = sysMenuService.getById(menu.getParentId()); parentType = parentMenu.getType(); } //目录、菜单 if(menu.getType() == Constant.MenuType.CATALOG.getValue() || menu.getType() == Constant.MenuType.MENU.getValue()){ if(parentType != Constant.MenuType.CATALOG.getValue()){ throw new RRException("上级菜单只能为目录类型"); } return ; } //按钮 if(menu.getType() == Constant.MenuType.BUTTON.getValue()){ if(parentType != Constant.MenuType.MENU.getValue()){ throw new RRException("上级菜单只能为菜单类型"); } return ; } }
可能小伙伴会说不加参数校验行不行?或者把参数校验放到前端不就行了?那你可是想的太简单了,世界比我们想象中的不安全,可能有“黑客”会绕过浏览器,直接使用 HTTP 工具,模拟请求向后端 API 接口传入违法的参数,以达到它们 “不可告人” 的目的。比如 sql 注入攻击,我相信,很多时候并不是我们不想添加,而是没有统一方便的方式,让我们快速的添加实现参数校验的功能。
世界上大多数碰到的困难,大多已经有了解决方案,特别是开发生态非常完整的java来说,早在 Java 2009 年就提出了 Bean Validation 规范,并且已经历经 JSR303、JSR349、JSR380 三次标准的置顶,发展到了 2.0 。
有细心的小伙伴可能发现 Jakarta Bean Validation 3.0 里,这里3.0变化并不大,只是更改了一下包名 和命名空间而已。实际上还是对 Bean Validation 2.0 的实现。
Bean Validation 和我们很久以前学习过的 JPA 一样,只提供规范,不提供具体的实现,目前实现 Bean Validation 规范的数据校验框架,主要有:
- Hibernate Validator
- Apache BVal
可能有小伙伴就要说 Hibernate 不就是个老掉牙的ORM框架吗?现在不都没人用了吗?其实不然 Hibernate 可是打着 Everything data 口号的,它还提供了 Hibernate Search、Hibernate OGM 等等解决方案的。
Hibernate 只是在国内的用的少了,国内主要是还是用 mybatis 这种 半orm框架的多。我们可以通过 google 的 trends 来看一下:
在中国的 mybatis, jpa, Hibernate 搜索热度:
在全球的 mybatis, jpa, Hibernate 搜索热度:
由于国内的开发环境可以说 99.99% 的开发者肯定都在用 spring,且正好 Spring Validation 提供了对 Bean Validation 的内置封装支持,可以使用 @Validated 注解,实现声明式校验,而无需直接调用 Bean Validation 提供的 API 方法。
而在实现原理上,也是基于 Spring AOP 拦截,最终还是调用不同的 Bean Validation 的实现框架。例如说,Hibernate Validator 。实现校验相关的操作这一点,类似 Spring Transaction 事务,通过 @Transactional 注解,实现声明式事务。下面,让我们开始学习如何在 Spring Boot 中,实现参数校验。
基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
二、注解
在开始入门之前,我们先了解下本文可能会涉及到的注解。javax.validation.constraints 包下,定义了一系列的约束( constraint )注解。共 22个,如下:
大致可以分为以下几类:
2.1 空和非空检查
@NotBlank
:只能用于字符串不为 null ,并且字符串#trim()
以后 length 要大于 0 。@NotEmpty
:集合对象的元素不为 0 ,即集合不为空,也可以用于字符串不为 null 。@NotNull
:不能为 null 。@Null
:必须为 null 。
2.2 数值检查
@DecimalMax(value)
:被注释的元素必须是一个数字,其值必须小于等于指定的最大值。@DecimalMin(value)
:被注释的元素必须是一个数字,其值必须大于等于指定的最小值。@Digits(integer, fraction)
:被注释的元素必须是一个数字,其值必须在可接受的范围内。@Positive
:判断正数。@PositiveOrZero
:判断正数或 0 。@Max(value)
:该字段的值只能小于或等于该值。@Min(value)
:该字段的值只能大于或等于该值。 -@Negative
:判断负数。@NegativeOrZero
:判断负数或 0 。
2.3 Boolean 值检查
@AssertFalse
:被注释的元素必须为 true 。@AssertTrue
:被注释的元素必须为 false 。
2.4 长度检查
@Size(max, min)
:检查该字段的 size 是否在 min 和 max 之间,可以是字符串、数组、集合、Map 等。
2.5 日期检查
@Future
:被注释的元素必须是一个将来的日期。@FutureOrPresent
:判断日期是否是将来或现在日期。@Past
:检查该字段的日期是在过去。@PastOrPresent
:判断日期是否是过去或现在日期。
2.6 其它检查
@Email
:被注释的元素必须是电子邮箱地址。@Pattern(value)
:被注释的元素必须符合指定的正则表达式。
2.7 Hibernate Validator 附加的约束注解
org.hibernate.validator.constraints
包下,定义了一系列的约束( constraint )注解。如下:
@Range(min=, max=)
:被注释的元素必须在合适的范围内。@Length(min=, max=)
:被注释的字符串的大小必须在指定的范围内。@URL(protocol=,host=,port=,regexp=,flags=)
:被注释的字符串必须是一个有效的 URL 。@SafeHtml
:判断提交的 HTML 是否安全。例如说,不能包含 javascript 脚本等等。
2.8 @Valid 和 @Validated
@Valid
注解,是 Bean Validation 所定义,可以添加在普通方法、构造方法、方法参数、方法返回、成员变量上,表示它们需要进行约束校验。
@Validated
注解,是 Spring Validation 锁定义,可以添加在类、方法参数、普通方法上,表示它们需要进行约束校验。同时,@Validated 有 value 属性,支持分组校验。属性如下:
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Validated { /** * Specify one or more validation groups to apply to the validation step * kicked off by this annotation. * <p>JSR-303 defines validation groups as custom annotations which an application declares * for the sole purpose of using them as type-safe group arguments, as implemented in * {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}. * <p>Other {@link org.springframework.validation.SmartValidator} implementations may * support class arguments in other ways as well. */ Class<?>[] value() default {}; }
对于初学的胖友来说,很容易搞混 @Valid(javax.validation
包下) 和 @Validated (org.springframework.validation.annotation
包下)注解。两者大致有以下的区别:
名称 | spring Validation是否实现了声明式校验 | 是否支持嵌套校验 | 是否支持分组校验 |
@Validated | 是 | 否 | 是 |
@Valid | 否 | 是 | 否 |
@Valid 有嵌套对象校验功能 例如说:如果不在 User.profile
属性上,添加 @Valid 注解,就会导致 UserProfile.nickname
属性,不会进行校验。
// User.java public class User { private String id; @Valid private UserProfile profile; } // UserProfile.java public class UserProfile { @NotBlank private String nickname; }
总的来说,绝大多数场景下,我们使用 @Validated 注解即可。而在有嵌套校验的场景,我们使用 @Valid 注解添加到成员属性上。
基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能
三、快速入门
3.1 引入依赖
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.4.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.ratel</groupId> <artifactId>java-validation</artifactId> <version>0.0.1-SNAPSHOT</version> <name>java-validation</name> <description>java validation action</description> <properties> <java.version>1.8</java.version> </properties> <dependencies> <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> <!--在一些高版本springboot中默认并不会引入这个依赖,需要手动引入--> <!-- <dependency> <groupId>org.hibernate.validator</groupId> <artifactId>hibernate-validator</artifactId> <scope>compile</scope> </dependency>--> <!-- 保证 Spring AOP 相关的依赖包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> </dependency> <!--lombok相关 方便开发--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <!--knife4j接口文档 方便待会进行接口测试--> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-spring-boot-starter</artifactId> <version>3.0.3</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
在 Spring Boot 体系中,也提供了 spring-boot-starter-validation
依赖。在这里,我们并没有引入。为什么呢? 因为 spring-boot-starter-web
已经引入了 spring-boot-starter-validation
,而 spring-boot-starter-validation
中也引入了 hibernate-validator
依赖,所以无需重复引入。三者的依赖引入关系可见下图
3.2 创建基本的类
UserAddDTO 实体类:
package com.ratel.validation.entity; import lombok.Data; import org.hibernate.validator.constraints.Length; import javax.validation.constraints.NotEmpty; import javax.validation.constraints.Pattern; /** * @Description * @Author ratelfu * @Date 2023/04/07 * @Version 1.0 */ @Data public class UserAddDTO { /** * 账号 */ @NotEmpty(message = "登录账号不能为空") @Length(min = 5, max = 16, message = "账号长度为 5-16 位") @Pattern(regexp = "^[A-Za-z0-9]+$", message = "账号格式为数字以及字母") private String username; /** * 密码 */ @NotEmpty(message = "密码不能为空") @Length(min = 4, max = 16, message = "密码长度为 4-16 位") private String password; }
UserController 用来写接口,在类上,添加 @Validated 注解,表示 UserController 是所有接口都需要进行参数校验。
package com.ratel.validation.cotroller; import com.ratel.validation.entity.UserAddDTO; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; import javax.validation.constraints.Min; @RestController @RequestMapping("/users") @Validated public class UserController { private Logger logger = LoggerFactory.getLogger(getClass()); @GetMapping("/get") public UserAddDTO get(@RequestParam("id") @Min(value = 1L, message = "编号必须大于 0") Integer id) { logger.info("[get][id: {}]", id); UserAddDTO userAddDTO = new UserAddDTO(); userAddDTO.setUsername("张三"); userAddDTO.setPassword("123456"); return userAddDTO; } @PostMapping("/add") public void add(@Valid @RequestBody UserAddDTO addDTO) { logger.info("[add][addDTO: {}]", addDTO); } }