Java Spring Boot 参数校验及自定义校验
在项目开发中,时常会碰到前端传递过来的请求参数需要校验,毕竟永远不要相信没有经过自己校验的数据,如果是零星几个参数,直接 if...else if ...else...
即可,但数据量大了,同时为了尽可能地增加复用,这里就可以用到参数校验
了,如果你觉得框架提供的校验方法不够用,或者你的校验比较个性化,那就自定义校验
。
环境:
- Spring Boot:3.1.6
- JDK:17
声明:
接下来的内容主要基于 [1] 做一定改动,如果想看原文,请点击原文,链接在下面参考中。
1.主要注解
先看常用注解有哪些:
可以看到注解主要集中于以下几个方面:
- 针对时间的,以前,现在,未来
- 针对数值型的,最小,最大,正负情况
- 针对字符串的,长度大小,是否空
- 针对布尔值的,true or false
以下是主要注解:
-
@NotNull
:值不能为null; -
@NotEmpty
:字符串、集合或数组的值不能为空,即长度大于0; -
@NotBlank
:字符串的值不能为空白,即不能只包含空格; -
@Size
:字符串、集合或数组的大小是否在指定范围内; -
@Min
:数值的最小值; -
@Max
:数值的最大值; -
@DecimalMin
:数值的最小值,可以包含小数; -
@DecimalMax
:数值的最大值,可以包含小数; -
@Digits
:数值是否符合指定的整数和小数位数; -
@Pattern
:字符串是否匹配指定的正则表达式; -
@Email
:字符串是否为有效的电子邮件地址; -
@AssertTrue
:布尔值是否为true; -
@AssertFalse
:布尔值是否为false; -
@Future
:日期是否为将来的日期; -
@Past
:日期是否为过去的日期;
2.注解使用
创建项目&&添加依赖
首先肯定还是先创建一个 Spring Boot web 项目,因为我们会用到参数校验,这里需要在 pom.xml 添加三方包依赖:
<!-- params validate --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
我们这里假设一种用户场景,后端根据前端提交过来的用户参数做校验,校验通过后,存入数据库中(项目演示为主,忽略数据库的使用),如果校验失败,将失败信息返回给前端。
校验的用户类
package com.example.springbootparamvalidatedemo.param; import com.example.springbootparamvalidatedemo.util.Phone; import jakarta.validation.constraints.*; import lombok.Data; @Data public class User { @NotBlank(message = "用户名不能为空") private String name; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 30, message = "密码长度在6到30之间") private String password; @Min(value = 18, message = "必须成年") @Max(value = 120, message = "不得超过年龄极限") private int age; @Pattern(regexp = "^(18[0-9])\d{8}$", message = "格式不正确") private String phone; public String getName() { return name; } public void setName(String name) { this.name = name; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } }
在用户类中,我们仅通过几个基本字段,根据字段属性,添加一定的校验注解。
统一的响应体
通常在项目开发中,我们都会用到统一的响应数据格式,所以这里将响应格式封装后,作为工具类供调用。
package com.example.springbootparamvalidatedemo.util; import lombok.Data; import java.io.Serializable; @Data public class Resp<T> implements Serializable { private int code; private boolean success; private T data; private String msg; private Resp(int code, T data, String msg) { this.code = code; this.data = data; this.msg = msg; this.success = code == 200; } public static <T> Resp<T> ok(T data) { return new Resp<>(200, data, null); } public static <T> Resp<T> error(String msg) { return new Resp<>(500, null, msg); } }
上面的响应体中,主要简单分为 成功和失败 的两种情况,考虑到通用,我们引入 泛型。
controller类中应用
这里就是简单根据前端传入的用户参数做校验,然后返回响应。
package com.example.springbootparamvalidatedemo.controller; import com.example.springbootparamvalidatedemo.param.User; import com.example.springbootparamvalidatedemo.util.Resp; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/user") public class UserController { @PostMapping("/save") public Resp save(@Valid @RequestBody User user) { // @Valid 表示校验参数 return Resp.ok(user); } }
为了让项目更加完善,引入全局异常处理,对最后的响应做拦截输出。
全局异常处理
package com.example.springbootparamvalidatedemo.exception; import com.example.springbootparamvalidatedemo.util.Resp; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler public Resp handleError(BindException e) { BindingResult bindingResult = e.getBindingResult(); return Resp.error(bindingResult.getFieldError().getDefaultMessage()); } }
校验演示
我们启动项目,然后在 postman 中测试看看。
这里假定传入了 age 不合规定的值:
传入全部合规的值:
从上面结果来看,参数校验是可以用的。
3.自定义注解
自定义实现
尽管框架提供一些校验规则,难免遇到一些现有规则不能覆盖的情况,这里我们就一些特定情况做个自定义的参数校验。
我们可以先观察下现有校验规则是怎样的:
Min
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(Min.List.class) @Documented @Constraint( validatedBy = {} ) public @interface Min { String message() default "{jakarta.validation.constraints.Min.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; long value(); @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface List { Min[] value(); } }
NotBlank
@Documented @Constraint( validatedBy = {} ) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Repeatable(NotBlank.List.class) public @interface NotBlank { String message() default "{jakarta.validation.constraints.NotBlank.message}"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface List { NotBlank[] value(); } }
理论上说,我们写个类似的注解接口,是不是就可以用呢?
实际上,在做自定义的参数校验的时候,除了自定义的注解接口,另外我们还需要再实现一个接口 ConstraintValidator
。
我们在 User 类中加个参数,比如加个用户地区,限定 Asia 或 Ameraica
,只有这两个地区的人才可以注册。
下面开始我们的自定义编码。
Address
package com.example.springbootparamvalidatedemo.util; import jakarta.validation.Constraint; import jakarta.validation.Payload; import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Constraint(validatedBy = {AddressValidator.class}) @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE}) public @interface Address { String message() default "不在合法地区范围内"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
实现类
package com.example.springbootparamvalidatedemo.util; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; import java.util.Arrays; public class AddressValidator implements ConstraintValidator<Address, String> { private static String[] addresses = {"Asia", "America"}; @Override public boolean isValid(String address, ConstraintValidatorContext context) { if (Arrays.asList(addresses).contains(address)) { return true; } return false; } }
在 User类 中加入 address 字段:
public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } @NotBlank(message = "地址非空") @Address private String address;
测试:
address 不在范围内的
address 在范围内
可以看到这里是生效了的。
自定义要点
注解接口:
-
@Constraint(validatedBy = {PhoneValidator.class})
:用于指定验证器类; -
@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE})
:指定@Phone
注解可以作用在方法、字段、构造函数、参数以及类型上
实现类[2]:
想让自定义验证注解生效,需要实现 ConstraintValidator
接口。接口的第一个参数是 自定义注解类型,第二个参数是 被注解字段的类型。这里因为订单ID 是 String 类型,我们第二个参数定义为 String 就可以了,需要提到的一点是 ConstraintValidator
接口的实现类无需添加 @Component
它在启动的时候就已经被加载到容器中了。
参考:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
2021-12-06 0127-单词接龙
2021-12-06 0841-钥匙与房间