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