写接口时,你是不是还在用一堆 if 判断参数是否为空、格式对不对?这篇文章带你用更优雅的方式搞定一切。
示例
小张 刚入职时,负责开发一个用户注册接口。他非常认真,编写了如下代码:
@postmapping("/register")
public string register(user user) {
// 手动校验每个字段
if (user.getusername() == null || user.getusername().isempty()) {
return "用户名不能为空";
}
if (user.getpassword() == null || user.getpassword().length() < 6) {
return "密码长度不能小于6位";
}
if (user.getemail() == null || !user.getemail().contains("@")) {
return "邮箱格式不正确";
}
if (user.getage() == null || user.getage() < 0 || user.getage() > 150) {
return "年龄不合法";
}
// ... 继续业务逻辑
}
这样写有什么问题?
| 问题 | 说明 |
|---|---|
| 代码臃肿 | 校验代码比业务逻辑还多 |
| 重复劳动 | 每个接口都要写一遍类似的校验 |
| 难以维护 | 新增字段要改多处代码 |
| 不统一 | 不同人写的校验格式五花八门 |
专业做法是:使用 java 的 bean validation(又叫 jsr-303)规范,通过注解优雅地完成参数校验。
五分钟快速入门
第一步:引入依赖
spring boot 2.3+ 需要手动引入校验依赖(老版本自带):
<dependency>
<groupid>org.springframework.boot</groupid>
<artifactid>spring-boot-starter-validation</artifactid>
</dependency>第二步:在实体类上加注解
import javax.validation.constraints.*;
public class userregisterdto {
@notblank(message = "用户名不能为空")
private string username;
@notblank(message = "密码不能为空")
@size(min = 6, max = 20, message = "密码长度必须在6-20位之间")
private string password;
@notblank(message = "邮箱不能为空")
@email(message = "邮箱格式不正确")
private string email;
@notnull(message = "年龄不能为空")
@min(value = 1, message = "年龄最小为1岁")
@max(value = 150, message = "年龄最大为150岁")
private integer age;
// getter / setter 省略
}
第三步:在controller中使用@valid
@restcontroller
@requestmapping("/api/user")
public class usercontroller {
@postmapping("/register")
public result register(@valid @requestbody userregisterdto dto) {
// 如果校验不通过,根本不会执行到这里
// 这里只管写业务逻辑
userservice.register(dto);
return result.success("注册成功");
}
}
就这样简单三步,所有 if 校验都不需要写了! 当参数不符合规则时,spring boot 会自动抛出异常,并返回400错误。
常用校验注解大全(新人必看)
空值校验
| 注解 | 适用类型 | 说明 |
|---|---|---|
@notnull | 任意类型 | 不能为 null |
@notblank | string | 不能为 null、空字符串、纯空格 |
@notempty | string、collection、map、数组 | 不能为 null 或空 |
使用建议
- 字符串字段优先用
@notblank - 集合/map 字段用
@notempty - 包装类型(如
integer、long)用@notnull
数值校验
| 注解 | 适用类型 | 说明 |
|---|---|---|
@min(value) | 数值类型 | 最小值(含) |
@max(value) | 数值类型 | 最大值(含) |
@decimalmin(value) | 数值类型 | 最小值(支持小数) |
@decimalmax(value) | 数值类型 | 最大值(支持小数) |
@digits(integer, fraction) | 数值类型 | 整数位数和小数位数限制 |
@positive | 数值类型 | 正数(>0) |
@positiveorzero | 数值类型 | 正数或0 |
@negative | 数值类型 | 负数 |
@negativeorzero | 数值类型 | 负数或0 |
示例
@min(value = 1, message = "数量至少为1") @max(value = 999, message = "数量不能超过999") private integer quantity; @decimalmin(value = "0.01", message = "金额至少为0.01") @decimalmax(value = "999999.99", message = "金额不能超过999999.99") private bigdecimal amount;
字符串校验
| 注解 | 说明 |
|---|---|
@size(min, max) | 字符串长度范围 |
@email | 邮箱格式 |
@pattern(regexp) | 正则表达式匹配 |
@url | url格式 |
示例
@past(message = "生日必须是过去的时间") private localdate birthday; @future(message = "有效期必须晚于当前时间") private localdatetime expiretime; @asserttrue(message = "必须同意用户协议") private boolean agreeprotocol;
分组校验:同一个对象,不同场景不同规则
同一个 dto 可能在不同接口中使用,校验规则不一样。比如:新增用户时密码必填,更新用户时密码可选。
第一步:定义分组接口(只是两个空接口)
public interface creategroup {} // 新增分组
public interface updategroup {} // 更新分组
第二步:在注解中指定分组
public class userdto {
@notnull(message = "id不能为空", groups = updategroup.class)
private long id;
@notblank(message = "用户名不能为空", groups = {creategroup.class, updategroup.class})
private string username;
@notblank(message = "密码不能为空", groups = creategroup.class) // 新增时必填
@size(min = 6, max = 20, message = "密码长度6-20位")
private string password;
@email(message = "邮箱格式不正确")
private string email; // 没指定分组,默认在所有分组都生效
}
第三步:在 controller 中指定使用的分组
@restcontroller
@requestmapping("/api/user")
public class usercontroller {
@postmapping("/create") // 新增时使用 creategroup
public result create(@validated(creategroup.class) @requestbody userdto dto) {
// 此时会校验:id(不校验,因为没有在creategroup中标记)、username(校验)、password(校验)
userservice.create(dto);
return result.success();
}
@putmapping("/update") // 更新时使用 updategroup
public result update(@validated(updategroup.class) @requestbody userdto dto) {
// 此时会校验:id(校验)、username(校验)、password(不校验,因为没有在updategroup中标记)
userservice.update(dto);
return result.success();
}
}
注意: 分组校验时要用 @validated 而不是 @valid,@validated 才能指定分组。
高级技巧:自定义校验注解
第一步:定义注解
import javax.validation.constraint;
import javax.validation.payload;
import java.lang.annotation.*;
@documented
@constraint(validatedby = gendervalidator.class) // 指定校验器
@target({elementtype.field})
@retention(retentionpolicy.runtime)
public @interface gender {
string message() default "性别只能是 male 或 female";
class<?>[] groups() default {};
class<? extends payload>[] payload() default {};
}
第二步:实现校验器
import javax.validation.constraintvalidator;
import javax.validation.constraintvalidatorcontext;
public class gendervalidator implements constraintvalidator<gender, string> {
@override
public boolean isvalid(string value, constraintvalidatorcontext context) {
if (value == null) {
return true; // 允许为空,由 @notnull 控制是否必填
}
return "male".equals(value) || "female".equals(value);
}
}
第三步:使用
public class userdto {
@gender(message = "性别只能填 male 或 female")
private string gender;
}
全局统一处理校验异常
默认情况下,校验失败会返回400错误和默认的报错信息。为了让前端收到统一格式的响应,需要全局异常处理。
import org.springframework.http.httpstatus;
import org.springframework.validation.fielderror;
import org.springframework.web.bind.methodargumentnotvalidexception;
import org.springframework.web.bind.annotation.exceptionhandler;
import org.springframework.web.bind.annotation.responsestatus;
import org.springframework.web.bind.annotation.restcontrolleradvice;
import java.util.hashmap;
import java.util.map;
@restcontrolleradvice
public class globalexceptionhandler {
/**
* 处理 @valid 校验失败异常
*/
@exceptionhandler(methodargumentnotvalidexception.class)
@responsestatus(httpstatus.bad_request)
public result handlevalidationexception(methodargumentnotvalidexception e) {
// 收集所有字段的校验失败信息
map<string, string> errors = new hashmap<>();
e.getbindingresult().getallerrors().foreach(error -> {
string fieldname = ((fielderror) error).getfield();
string errormessage = error.getdefaultmessage();
errors.put(fieldname, errormessage);
});
return result.error(400, "参数校验失败", errors);
}
}
返回给前端的格式
{
"code": 400,
"message": "参数校验失败",
"data": {
"username": "用户名不能为空",
"password": "密码长度必须在6-20位之间"
}
}常见问题与避坑指南
坑一:@valid 不生效
原因: 没有引入 spring-boot-starter-validation 依赖(spring boot 2.3+ 需要手动引入)。
解决方案: 检查 pom.xml 是否有该依赖。
坑二:对 list 集合校验无效
错误写法:
@postmapping("/batch")
public result batch(@valid @requestbody list<userdto> userlist) { // list 不支持 @valid
// ...
}
正确写法: 用包装类
@data
public class userlistdto {
@valid
private list<userdto> userlist;
}
@postmapping("/batch")
public result batch(@valid @requestbody userlistdto dto) {
// ...
}
坑三:嵌套对象校验失效
错误写法:
public class orderdto {
@notnull
private long userid;
// 没有加 @valid,address 内部的校验不生效
private addressdto address;
}
正确写法:
public class orderdto {
@notnull
private long userid;
@valid // 必须加 @valid 才能触发嵌套校验
private addressdto address;
}
坑四:整数类型的 @notnull 无法校验 0
@notnull 只校验是否为 null,不校验值的大小。如果要排除 0,需要配合 @min(1) 使用。
坑五:日志记录时泄露敏感信息
校验失败时如果直接把整个dto对象打印到日志,可能泄露密码等信息。
错误做法:
logger.error("校验失败,参数:{}", dto); // 可能包含密码
正确做法:
logger.error("校验失败,用户:{},字段:{}", dto.getusername(), errors);
检查清单
为了帮助你快速检查自己的项目是否已正确使用校验功能,这里提供一个检查清单表格:
| 检查项 | 说明 |
|---|---|
项目中已引入 spring-boot-starter-validation 依赖 | spring boot 2.3+ 需要手动引入 |
所有接口参数都用 dto 接收,配合 @valid 或 @validated 校验 | 避免在 controller 中写大量 if 判断 |
| 同一个 dto 在不同接口有不同校验规则时,使用了分组校验 | 通过 @validated(group.class) 指定分组 |
| 内置注解无法满足时,写了自己的自定义注解 | 实现 constraintvalidator 接口 |
全局异常处理器统一处理 methodargumentnotvalidexception | 返回统一格式的错误响应 |
嵌套对象校验用了 @valid | 确保嵌套对象内部的注解生效 |
| list 集合校验用了包装类 | 直接对 list<t> 使用 @valid 无效 |
| 没有在日志中记录密码等敏感信息 | 避免泄露用户隐私 |
最后
从手动写 if 校验到使用注解校验,代码量减少 80% 以上,可读性和维护性却大幅提升。这就是用好工具的价值——把时间花在真正的业务逻辑上,而不是重复的校验劳动上。
以上就是springboot接口参数校验从入门到精通的完整指南的详细内容,更多关于springboot接口参数校验的资料请关注代码网其它相关文章!
发表评论