SpringBoot整合参数校验

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)的参考实现。

docs.jboss.org/hibernate/s...

官网介绍:

验证数据是一项常见任务,它发生在从表示层到持久层的所有应用程序层中。通常在每一层都实现相同的验证逻辑,这既耗时又容易出错。为了避免重复这些验证,开发人员经常将验证逻辑直接捆绑到域模型中,将域类与验证代码混在一起,而验证代码实际上是关于类本身的元数据。

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是否可以通过验证
@Email String 该字段为Email格式,才能通过 YES
@Length 被注释的字符串的大小必须在指定的范围内 被注释的字符串的大小必须在指定的范围内 NO
@NotEmpty String、集合、数组、Map、链表List 不能为null,不能是空字符,集合、数组、map等size()不能为0;字符串trim()后可以等于"" NO

Hibernate Validator 不同版本附加的 Constraint 可能不太一样,具体还需要你自己查看你使用版本 。Hibernate 提供的 Constraintorg.hibernate.validator.constraints这个包下面。

一个 constraint 通常由 annotation 和相应的 constraint validator 组成,它们是一对多的关系。也就是说可以有多个 constraint validator 对应一个 annotation。在运行时,Bean Validation 框架本身会根据被注释元素的类型来选择合适的 constraint validator 对数据进行验证。

有些时候,在用户的应用中需要一些更复杂的 constraint。Bean Validation 提供扩展 constraint 的机制。可以通过两种方法去实现,一种是组合现有的 constraint 来生成一个更复杂的 constraint,另外一种是开发一个全新的 constraint。

对于web服务来说,为防止非法参数对业务造成影响,在Controller层一定要做参数校验的!大部分情况下,请求参数分为如下两种形式:

  1. POSTPUT请求,使用requestBody传递参数;
  2. 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 校验信息的国际化

  1. 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;
}
  1. 添加上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长度不正确
  1. 返回结果
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...

相关推荐
2401_8543910811 分钟前
城镇住房保障:SpringBoot系统功能概览
java·spring boot·后端
hummhumm13 分钟前
Oracle 第29章:Oracle数据库未来展望
java·开发语言·数据库·python·sql·oracle·database
wainyz22 分钟前
Java NIO操作
java·开发语言·nio
工业3D_大熊27 分钟前
【虚拟仿真】CEETRON SDK在船舶流体与结构仿真中的应用解读
java·python·科技·信息可视化·c#·制造·虚拟现实
lzb_kkk36 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
爬山算法1 小时前
Maven(28)如何使用Maven进行依赖解析?
java·maven
2401_857439691 小时前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索1 小时前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
芒果披萨2 小时前
Filter和Listener
java·filter