写接口时,你是不是还在用一堆 if 判断参数是否为空、格式对不对?这篇文章带你用更优雅的方式搞定一切。
示例
小张刚入职时,负责开发一个用户注册接口。他非常认真,编写了如下代码:
java
@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+ 需要手动引入校验依赖(老版本自带):
java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
第二步:在实体类上加注解
java
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
java
@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 |
示例
java
@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格式 |
示例
java
@Past(message = "生日必须是过去的时间")
private LocalDate birthday;
@Future(message = "有效期必须晚于当前时间")
private LocalDateTime expireTime;
@AssertTrue(message = "必须同意用户协议")
private Boolean agreeProtocol;
分组校验:同一个对象,不同场景不同规则
同一个 DTO 可能在不同接口中使用,校验规则不一样。比如:新增用户 时密码必填,更新用户时密码可选。
第一步:定义分组接口(只是两个空接口)
java
public interface CreateGroup {} // 新增分组
public interface UpdateGroup {} // 更新分组
第二步:在注解中指定分组
java
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 中指定使用的分组
java
@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 才能指定分组。
高级技巧:自定义校验注解
第一步:定义注解
java
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 {};
}
第二步:实现校验器
java
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);
}
}
第三步:使用
java
public class UserDTO {
@Gender(message = "性别只能填 MALE 或 FEMALE")
private String gender;
}
全局统一处理校验异常
默认情况下,校验失败会返回400错误和默认的报错信息。为了让前端收到统一格式的响应,需要全局异常处理。
java
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);
}
}
返回给前端的格式
java
{
"code": 400,
"message": "参数校验失败",
"data": {
"username": "用户名不能为空",
"password": "密码长度必须在6-20位之间"
}
}
常见问题与避坑指南
坑一:@Valid 不生效
原因: 没有引入 spring-boot-starter-validation 依赖(Spring Boot 2.3+ 需要手动引入)。
解决方案: 检查 pom.xml 是否有该依赖。
坑二:对 List 集合校验无效
错误写法:
java
@PostMapping("/batch")
public Result batch(@Valid @RequestBody List<UserDTO> userList) { // List 不支持 @Valid
// ...
}
正确写法: 用包装类
java
@Data
public class UserListDTO {
@Valid
private List<UserDTO> userList;
}
@PostMapping("/batch")
public Result batch(@Valid @RequestBody UserListDTO dto) {
// ...
}
坑三:嵌套对象校验失效
错误写法:
java
public class OrderDTO {
@NotNull
private Long userId;
// 没有加 @Valid,Address 内部的校验不生效
private AddressDTO address;
}
正确写法:
java
public class OrderDTO {
@NotNull
private Long userId;
@Valid // 必须加 @Valid 才能触发嵌套校验
private AddressDTO address;
}
坑四:整数类型的 @NotNull 无法校验 0
@NotNull 只校验是否为 null,不校验值的大小。如果要排除 0,需要配合 @Min(1) 使用。
坑五:日志记录时泄露敏感信息
校验失败时如果直接把整个DTO对象打印到日志,可能泄露密码等信息。
错误做法:
java
logger.error("校验失败,参数:{}", dto); // 可能包含密码
正确做法:
java
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% 以上,可读性和维护性却大幅提升。这就是用好工具的价值------把时间花在真正的业务逻辑上,而不是重复的校验劳动上。