SpringBoot整合参数校验
1.前言☕
大家好,我是Leo哥🫣🫣🫣,今天给大家带来关于精品SpringBoot专栏,暂且就给他起名为循序渐进学SpringBoot,这里我参考了我上一个专栏:循序渐进学SpringSecurity6。有需要的朋友可以抓紧学习来哈,带你从SpringSecurity从零到实战项目。好了,我们进入正题,为什么会有SpringBoot这个专栏呢,是这样的,今年Leo哥也是正在重塑知识体系,从基础到框架,而SpringBoot又是我们框架中的核心,我觉得很有必要通过以博客的形式将我的知识系列进行输出,同时也锻炼一下自己的写作能力,如果能帮到大家那就更好啦!!!本地系列教程会从SpringBoot基础讲起,会以知识点+实例+项目的学习模式由浅入深对Spring Boot框架进行学习&使用。好了,话不多说让我们开始吧😎😎😎。
2.什么是JSR
JSR(Java Specification Requests)」 是一套 JavaBean 参数校验的标准,它定义了很多常用的校验注解,我们可以直接将这些注解加在我们 JavaBean 的属性上面,这样就可以在需要校验的时候进行校验了,非常方便!
校验的时候我们实际用的是 「Hibernate Validator」 框架。Hibernate Validator 是 Hibernate 团队最初的数据校验框架,Hibernate Validator 4.x 是 Bean Validation 1.0(JSR 303)的参考实现,Hibernate Validator 5.x 是 Bean Validation 1.1(JSR 349)的参考实现,目前最新版的 Hibernate Validator 6.x 是 Bean Validation 2.0(JSR 380)的参考实现。
官网介绍:
验证数据是一项常见任务,它发生在从表示层到持久层的所有应用程序层中。通常在每一层都实现相同的验证逻辑,这既耗时又容易出错。为了避免重复这些验证,开发人员经常将验证逻辑直接捆绑到域模型中,将域类与验证代码混在一起,而验证代码实际上是关于类本身的元数据。

3.内嵌注解
Bean Validation 内嵌的注解很多,基本实际开发中已经够用了,注解如下:
| 注解 | 作用类型 | 解释 | null是否能通过验证 |
|---|---|---|---|
| @AssertFalse | Boolean、boolean | 该字段值为false时,验证才能通过 | YES |
| @AssertTrue | Boolean、boolean | 该字段值为true时,验证才能通过 | YES |
| @DecimalMax | 数字类型(原子和包装) | 验证小数的最大值@DecimalMax(value = "12.35") private double money; |
YES |
| @DecimalMin | 数字类型(原子和包装) | 验证小数的最小值 | YES |
| @Digits | 数字类型(原子和包装) | 验证数字的整数位和小数位的位数是否超过指定的长度@Digits(integer = 2, fraction = 2) private double money; |
YES |
| @Future | 时期、时间 | 验证日期是否在当前时间之后,否则无法通过校验@Future private Date date; |
YES |
| @FutureOrPresent | 时期、时间 | 时间在当前时间之后 或者等于此时 | YES |
| @Max | 数字类型(原子和包装) | //该字段的最大值为18,否则无法通过验证 @Max(value = 18) private Integer age; |
YES |
| @Min | 数字类型(原子和包装) | 同上,不能低于某个值否则无法通过验证 | YES |
| @Negative | 数字<0 | YES | |
| @NegativeOrZero | 数字=<0 | YES | |
| @NotBlank | String 该注解用来判断字符串或者字符,只用在String上面 | 字符串不能为null,字符串trim()后也不能等于"" | NO |
| @NotNull | 任何类型 | 使用该注解的字段的值不能为null,否则验证无法通过 | NO |
| @Null | 修饰的字段在验证时必须是null,否则验证无法通过 | YES | |
| @Past | 时间、日期 | 验证日期是否在当前时间之前,否则无法通过校验,必须是一个过去的时间或日期 | YES |
| @PastOrPresent | 时间、日期 | 验证日期是否在当前时间之前或等于当前时间 | YES |
| @Pattern | 用于验证字段是否与给定的正则相匹配@Pattern(regexp = "正则") private String name; |
YES | |
| @Positive | 数字>0 | YES | |
| @PositiveOrZero | 数字>=0 | YES | |
| @Size | 字符串String、集合Set、数组Array、Map,List | 修饰的字段长度不能超过5或者低于1 @Size(min = 1, max = 5) private String name;集合、数组、map等的size()值必须在指定范围内//只能一个 @Size(min = 1, max = 1) private List<String> names; |
YES |
以上是
Bean Validation的内嵌的注解,以下是 Hibernate Validator 附加的 constraint
| 注解 | 作用类型 | 解释 | null是否可以通过验证 |
|---|---|---|---|
| String | 该字段为Email格式,才能通过 | YES | |
| @Length | 被注释的字符串的大小必须在指定的范围内 | 被注释的字符串的大小必须在指定的范围内 | NO |
| @NotEmpty | String、集合、数组、Map、链表List | 不能为null,不能是空字符,集合、数组、map等size()不能为0;字符串trim()后可以等于"" | NO |
Hibernate Validator 不同版本附加的 Constraint 可能不太一样,具体还需要你自己查看你使用版本 。Hibernate 提供的 Constraint 在
org.hibernate.validator.constraints这个包下面。一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。
有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制。可以通过两种方法去实现,一种是组合现有的 constraint 来生成一个更复杂的 constraint,另外一种是开发一个全新的 constraint。
对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:
POST、PUT请求,使用requestBody传递参数;GET请求,使用requestParam/PathVariable传递参数。
下面我们简单介绍下SpringBoot整合参数校验实战!
4.SpringBoot整合参数校验
环境说明:
该文章基于 springboot-2.3.12.RELEASE 和 hibernate-validator.6.1.7 版本
4.1 添加依赖
xml
<!--对应hibernate-validator版本6.1.7-final -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
注:从springboot-2.3开始,校验包被独立成了一个starter组件,所以需要引入validation和web,而springboot-2.3之前的版本只需要引入 web 依赖就可以了。
4.2 环境准备
1. 实体类准备
arduino
package org.javatop.validator.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author : Leo
* @version 1.0
* @date 2024-04-02 21:58
* @description : 实体类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
/**
* 主键id
*/
private Integer id;
/**
* 用户名
*/
private String name;
/**
* 年龄
*/
private Integer age;
/**
* 性别
*/
private String sex;
/**
* 住址
*/
private String address;
/**
* 邮箱
*/
private String email;
}
2. UserDto
kotlin
package org.javatop.validator.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Email;
/**
* @author : Leo
* @version 1.0
* @date 2024-04-02 23:34
* @description : DTO
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserDto {
private Integer userId;
@NotEmpty(message = "姓名不能为空")
private String name;
@Range(min = 18,max = 50,message = "年龄必须在18和50之间")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
4.3 直接参数校验
有时候接口的参数比较少,只有一个或者两三个参数,这时候就没必要定义一个DTO来接收参数,可以直接接收参数。
less
/**
* (user)表控制层
*
* @author Leo
*/
@Validated
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
/**
* 根据id查询用户信息
* @param id id
* @return 注意:如果想在参数中使用 @NotNull 这种注解校验,就必须在类上添加 @Validated
*/
@GetMapping("/getUser")
public UserDto getUser(@NotNull(message = "用户id不能为空") Integer id){
log.info("id: {}", id);
UserDto userDto = new UserDto();
userDto.setName("程序员Leo");
userDto.setAge(18);
userDto.setEmail("Leo@qq.com");
return userDto;
}
}
代码测试:

4.4 实体类DTO参数校验
UserDto我们在上面已经定义过了。
接收参数时使用@Validated进行校验
less
/**
* 添加用户
* @param userDto 用户DTO
* @return 注意:如果想在参数中使用 @NotNull 这种注解校验,就必须在类上添加 @Validated
*/
@PostMapping("/saveUser")
public String getUser(@Validated @RequestBody UserDto userDto){
userDto.setId(100);
return "ok";
}
代码测试:

4.5 对Service层进行参数校验
个人不太喜欢这种校验方式,一般情况下调用service层方法的参数都需要在controller层校验好,不需要再校验一次。这边只是列举这个功能,告诉大家在Spring中还是支持这个功能的。
less
@Validated
@Service
@Slf4j
public class ValidatorService {
public String showAge(@NotNull(message = "不能为空") @Min(value = 18, message = "最小18") String age) {
logger.info("age = {}", age);
return age;
}
}
4.6 分组校验
有时候对于不同的接口,需要对DTO进行不同的校验规则。还是以上面的UserDTO为列,另外一个接口可能不需要将age限制在18~50之间,只需要大于18就可以了。
这样上面的校验规则就不适用了。分组校验就是来解决这个问题的,同一个DTO,不同的分组采用不同的校验策略。
修改UserDto的校验规则,添加分组校验。
kotlin
package org.javatop.validator.domain;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.NotEmpty;
import org.hibernate.validator.constraints.Range;
import javax.validation.constraints.Email;
/**
* @author : Leo
* @version 1.0
* @date 2024-04-02 23:34
* @description : DTO
*/
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserDto {
public interface Default {
}
public interface Group1 {
}
private Integer id;
//注意:@Validated 注解中加上groups属性后,DTO中没有加group属性的校验规则将失效
@NotEmpty(message = "姓名不能为空", groups = Default.class)
private String name;
// 注意:加了groups属性之后,必须在@Validated 注解中也加上groups属性后,校验规则才能生效,不然下面的校验限制就失效了
@Range(min = 18, max = 50, message = "年龄必须在18和50之间", groups = Default.class)
@Range(min = 17, message = "年龄必须大于17", groups = Group1.class)
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
less
/**
* 添加用户
* @param userDto 用户DTO
* @return 注意:如果方法中的参数是对象类型,则必须要在参数对象前面添加 @Validated 进行分组校验,年龄满足大于17
*/
@PostMapping("/saveUserGroup")
public String saveUserGroup(@Validated(value = {UserDto.Group1.class}) @RequestBody UserDto userDto){
userDto.setId(100);
return "ok";
}
代码测试:

我们切换一下默认的规则试试。

代码测试:
看到这里的提示信息就变成了我们默认的分组规则。

使用Group1分组进行校验,因为Dto中,Group1分组对name属性没有校验,所以这个校验将不会生效。
分组校验的好处是可以对同一个DTO设置不同的校验规则,缺点就是对于每一个新的校验分组,都需要重新设置下这个分组下面每个属性的校验规则。
4.7 嵌套校验
前面的代码示例中,Dto类里面的字段都是基本数据类型和String等类型。
但是实际场景中,有可能某个字段也是一个对象,如果我们需要对这个对象里面的数据也进行校验,可以使用嵌套校验。
假如UserDto中还用一个Cat对象,比如下面的结构。需要注意的是,在Cat类的校验上面一定要加上@Valid注解。
添加Cat类
less
/**
* @author : Leo
* @version 1.0
* @date 2024-04-03 00:10
* @description : Cat类
*/
@Validated
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Cat {
@NotEmpty(message = "姓名不能为空")
private String name;
private String color;
}
修改User类
less
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserDto {
public interface Default {
}
public interface Group1 {
}
private Integer id;
//注意:@Validated 注解中加上groups属性后,DTO中没有加group属性的校验规则将失效
@NotEmpty(message = "姓名不能为空", groups = Default.class)
private String name;
// 注意:加了groups属性之后,必须在@Validated 注解中也加上groups属性后,校验规则才能生效,不然下面的校验限制就失效了
@Range(min = 18, max = 50, message = "年龄必须在18和50之间", groups = Default.class)
@Range(min = 17, message = "年龄必须大于17", groups = Group1.class)
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
@Valid
@NotNull
private Cat cat;
}
less
@PostMapping("/saveUserWithCat")
public String saveUserWithJob(@Validated @RequestBody UserDto userDto){
userDto.setId(100);
return "ok";
}
代码测试:

嵌套校验可以结合分组校验一起使用。还有就是嵌套集合校验会对集合里面的每一项都进行校验,例如List字段会对这个list里面的每一个Cat对象都进行校验
4.8 自定义校验器
在Spring中自定义校验器非常简单,下面跟着Leo的视角走起。
1. 自定义约束注解
less
/**
* @author : Leo
* @version 1.0
* @date 2024-04-03 00:21
* @description : 自定义约束注解
*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER})
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {EncryptIdValidator.class})
public @interface EncryptId {
// 默认错误消息
String message() default "加密id格式错误";
// 分组
Class[] groups() default {};
// 负载
Class[] payload() default {};
}
2. 实现ConstraintValidator接口编写约束校验器
java
/**
* @author : Leo
* @version 1.0
* @date 2024-04-03 00:23
* @description : 实现ConstraintValidator接口编写约束校验器
*/
public class EncryptIdValidator implements ConstraintValidator<EncryptId, String> {
private static final Pattern PATTERN = Pattern.compile("^[a-f\d]{32,256}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// 不为null才进行校验
if (value != null) {
Matcher matcher = PATTERN.matcher(value);
return matcher.find();
}
return true;
}
}
4.9 编程式校验
上面的代码都是基于注解来实现自动校验的,在我们特殊的业务场景中,我们可能希望以编程方式调用验证。这个时候可以注入 javax.validation.Validator对象,然后再调用其api。
less
@Autowired
private javax.validation.Validator globalValidator;
// 编程式校验
@PostMapping("/saveWithCodingValidate")
public String saveWithCodingValidate(@RequestBody UserDto userDto) {
Set<constraintviolation> validate = globalValidator.validate(userDto, UserDto.Save.class);
// 如果校验通过,validate为空;否则,validate包含未校验通过项
if (validate.isEmpty()) {
// 校验通过,才会执行业务逻辑处理
} else {
for (ConstraintViolation userDtoConstraintViolation : validate) {
// 校验失败,做其它逻辑
System.out.println(userDtoConstraintViolation);
}
}
return "ok";
}
4.10 校验信息的国际化
- bean 中添加标签 标签需要加在属性上,@NotEmpty标签String的参数不能为空
less
@Data
public class DemoDto {
@NotEmpty(message = "{demo.key.null}")
@Length(min = 5, max = 25, message = "{demo.key.length}")
private String key;
}
-
添加上ValidationMessages文件
国际化配置文件必须放在classpath的根目录下,即src/java/resources的根目录下。 国际化配置文件必须以ValidationMessages开头,比如ValidationMessages.properties 或者 ValidationMessages_en.properties。 在/resources的根目录下添加上ValidationMessages.properties文件
ini
demo.key.null = demo的key不能为空,这里是validationMessage
demo.key.length = demo的key长度不正确
- 返回结果
arduino
"demo的key不能为空,这里是validationMessage;"
自定义properties文件
SpringBoot 国际化验证 @Validated 的 message 国际化资源文件默认必须放在 resources/ValidationMessages.properties 中。 现在我想把资源文件放到 resources/message/messages_zh.properties 中。
若要自定义文件位置或名称则需要重写WebMvcConfigurerAdapter 的 getValidator 方法,但WebMvcConfigurerAdapter在springboot2中已经废弃了,可以改为使用WebMvcConfigurationSupport
在一的基础上修改:
scala
@Configuration
public class ValidatorConfiguration extends WebMvcConfigurationSupport {
@Autowired
private MessageSource messageSource;
@Override
public Validator getValidator() {
return validator();
}
@Bean
public Validator validator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource);
return validator;
}
}
5.@Valid和@Validated区别
@Valid 和 @Validated 都是用于校验数据的注解,尽管它们听起来相似并且用途相近,但实际上在使用场景和功能上存在一些关键区别。
@Valid
@Valid注解是由 JSR 303(Bean Validation 1.0)和 JSR 349(Bean Validation 1.1)规范定义的。- 它可以用于任何受支持的对象上,主要用于校验被注解的对象的属性值是否符合约束条件。
@Valid通常与 Spring 的@RequestBody或 JPA 实体一起使用,用来确保请求体或实体属性满足验证约束。- 它是在方法参数级别使用的,不能通过它来应用验证组(Validation groups)或者自定义的验证逻辑。
@Validated
@Validated是 Spring 框架特有的注解,它扩展了@Valid的功能。- 除了支持
@Valid的基本校验功能外,@Validated还支持分组校验。这意味着你可以定义验证组来进行更细粒度的验证控制,比如在创建时验证一组约束,在更新时验证另一组约束。 @Validated可以用于方法级别和类级别。在类级别使用时,它通常应用于 Spring 的组件(比如,@Controller或@Service),以便对该组件的所有方法应用验证策略。- 它允许在方法参数级别进行嵌套路径的校验,这在处理复杂的嵌套对象时非常有用。
使用场景比较
- 当你只需要进行基本的数据绑定和校验时,使用
@Valid是足够的。 - 当你需要更复杂的校验,比如校验分组,或者你想要在特定的Spring组件级别应用校验策略时,使用
@Validated会更合适。
示例代码
使用 @Valid 对请求体进行校验:
less
@PostMapping("/user")
public String createUser(@RequestBody @Valid User user) {
// 处理
}
使用 @Validated 进行分组校验:
less
@Controller
@Validated(OnCreate.class)
public class UserController {
@PostMapping("/user")
public String createUser(@Validated(OnCreate.class) @RequestBody User user) {
// 处理
}
}
总结起来,@Valid 和 @Validated 都是用于数据验证的有力工具,但 @Validated 提供了更高级的特性,比如分组校验,适合在更复杂的校验场景中使用。
6.源码仓库
Github源码:github.com/gaoziman/Le...