外观
Spring Boot 参数校验
约 2111 字大约 7 分钟
2025-08-16
一、基础设置与常用注解
1. 依赖引入
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>提示:Spring Boot 2.3 及以上版本需要显式添加此依赖,之前版本已包含在
spring-boot-starter-web中。
2. 常用校验注解
字符串校验:
| 注解 | 说明 | 示例 |
|---|---|---|
@NotBlank | 字符串非空且至少包含一个非空白字符 | @NotBlank(message = "用户名不能为空") |
@NotEmpty | 集合、数组、Map 或字符串不为空 | @NotEmpty(message = "列表不能为空") |
@Size(min=, max=) | 字符串长度或集合大小 | @Size(min = 6, max = 20, message = "密码长度需在6-20之间") |
@Email | 邮箱格式校验 | @Email(message = "邮箱格式不正确") |
@Pattern(regexp=) | 正则表达式匹配 | @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") |
数字校验:
| 注解 | 说明 | 示例 |
|---|---|---|
@Min(value) | 最小值(适用于数字) | @Min(value = 18, message = "年龄不能小于18") |
@Max(value) | 最大值(适用于数字) | @Max(value = 100, message = "年龄不能大于100") |
@DecimalMin(value) | 最小值(适用于BigDecimal) | @DecimalMin(value = "0.01", message = "金额不能小于0.01") |
@DecimalMax(value) | 最大值(适用于BigDecimal) | @DecimalMax(value = "1000000", message = "金额不能大于1000000") |
其他常用校验:
| 注解 | 说明 | 示例 |
|---|---|---|
@NotNull | 不能为null(适用于对象) | @NotNull(message = "ID不能为空") |
@Null | 必须为null | @Null(message = "此字段必须为空") |
@AssertTrue | 必须为true | @AssertTrue(message = "必须同意服务条款") |
@Past / @Future | 过去/将来的时间 | @Past(message = "生日必须是过去的时间") |
提示:
@NotBlank只适用于字符串,会检查字符串是否为null或空白;@NotEmpty适用于字符串、集合、数组和Map;@NotNull只检查是否为null。
3. 实体类定义
请求参数封装:
import lombok.Data;
import javax.validation.constraints.*;
@Data
public class UserParam {
@NotNull(message = "ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度需在2-20之间")
private String username;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Min(value = 18, message = "年龄不能小于18")
@Max(value = 100, message = "年龄不能大于100")
private Integer age;
}提示:将请求参数封装到专用DTO类中,而不是直接使用实体类,遵循单一职责原则。
二、校验实现与错误处理
1. Controller 层校验
基本用法:
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody UserParam userParam,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
// 处理校验错误
String errorMessage = bindingResult.getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest().body(errorMessage);
}
// 处理正常逻辑
return ResponseEntity.ok("用户创建成功");
}
}全局异常处理:
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 {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationExceptions(
MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getAllErrors().forEach((error) -> {
String fieldName = ((FieldError) error).getField();
String errorMessage = error.getDefaultMessage();
errors.put(fieldName, errorMessage);
});
return errors;
}
}提示:使用全局异常处理器可以避免在每个Controller中重复处理校验错误,使代码更简洁。
2. 嵌套对象校验
当DTO包含嵌套对象时:
@Data
public class UserParam {
@NotBlank(message = "用户名不能为空")
private String username;
@Valid // 必须添加此注解
@NotNull(message = "地址信息不能为空")
private AddressParam address;
}
@Data
public class AddressParam {
@NotBlank(message = "省不能为空")
private String province;
@NotBlank(message = "市不能为空")
private String city;
}提示:嵌套对象校验需要在字段上添加
@Valid注解,否则内部对象的校验不会生效。
三、分组校验
1. 分组校验场景
适用场景:
- 同一DTO在不同场景下需要不同的校验规则
- 例如:创建用户和更新用户时,ID字段的校验规则不同
- 创建:ID应为null
- 更新:ID不应为null
2. 分组校验实现
定义分组接口:
public interface AddValidationGroup {}
public interface EditValidationGroup {}在DTO中使用分组:
@Data
public class UserParam {
@Null(groups = AddValidationGroup.class, message = "新增时ID必须为空")
@NotNull(groups = EditValidationGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度需在2-20之间")
private String username;
// 其他字段...
}Controller中使用分组:
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping("/add")
public ResponseEntity<String> addUser(
@Validated(AddValidationGroup.class) @RequestBody UserParam userParam) {
// 业务逻辑
return ResponseEntity.ok("用户添加成功");
}
@PostMapping("/edit")
public ResponseEntity<String> editUser(
@Validated(EditValidationGroup.class) @RequestBody UserParam userParam) {
// 业务逻辑
return ResponseEntity.ok("用户更新成功");
}
}提示:分组校验时必须使用
@Validated注解,@Valid不支持分组功能。
四、自定义校验
1. 自定义注解
定义注解:
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = TelephoneNumberValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface TelephoneNumber {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}2. 自定义校验器
实现ConstraintValidator接口:
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class TelephoneNumberValidator
implements ConstraintValidator<TelephoneNumber, String> {
private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
@Override
public void initialize(TelephoneNumber constraintAnnotation) {
// 初始化逻辑(可选)
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// null值由@NotNull处理
if (value == null) {
return true;
}
return value.matches(PHONE_REGEX);
}
}3. 使用自定义注解
在DTO中使用:
@Data
public class UserParam {
@TelephoneNumber
private String phone;
// 其他字段...
}提示:自定义校验器可以处理复杂的业务规则,但应避免过度设计。简单规则优先考虑使用
@Pattern。
五、最佳实践与常见问题
1. 校验位置选择
校验层级:
- 前端校验:提供即时反馈,但可绕过,不能作为唯一校验
- Controller层校验:必须的,防止非法数据进入业务逻辑
- Service层校验:针对复杂业务规则,确保数据一致性
建议:
- 基本格式校验放在Controller层
- 业务规则校验放在Service层
- 不要完全依赖前端校验
2. 错误消息处理
国际化支持:
# ValidationMessages.properties
NotBlank.userParam.username=用户名不能为空
Size.userParam.username=用户名长度需在{min}-{max}之间
Email.userParam.email=邮箱格式不正确自定义错误消息格式:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public Validator getValidator() {
LocalValidatorFactoryBean factory = new LocalValidatorFactoryBean();
MessageSource messageSource = new ResourceBundleMessageSource();
messageSource.setBasename("ValidationMessages");
factory.setValidationMessageSource(messageSource);
return factory;
}
}3. 常见问题与解决方案
问题1:校验不生效
- 原因:忘记添加
@Valid或@Validated注解 - 解决方案:确保在需要校验的参数前添加校验注解
问题2:嵌套对象校验不生效
- 原因:嵌套对象字段缺少
@Valid注解 - 解决方案:在嵌套对象字段上添加
@Valid注解
问题3:分组校验不生效
- 原因:使用了
@Valid而不是@Validated - 解决方案:分组校验必须使用
@Validated
问题4:自定义校验器不被调用
- 原因:
isValid方法返回值错误或正则表达式不正确 - 解决方案:检查校验逻辑,确保null值处理正确
4. @Valid vs @Validated 对比
| 特性 | @Valid | @Validated |
|---|---|---|
| 分组校验 | 不支持 | 支持 |
| 注解位置 | 可用于方法参数、字段 | 可用于方法、类、方法参数 |
| 嵌套校验 | 支持 | 支持 |
| 所属包 | javax.validation | org.springframework.validation |
| 适用场景 | 基本校验 | 需要分组校验的场景 |
提示:一般场景下两者可互换,但需要分组校验时必须使用
@Validated。
六、实用技巧速查表
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 基本参数校验 | @Valid + 全局异常处理 | 需要添加spring-boot-starter-validation依赖 |
| 嵌套对象校验 | @Valid + 嵌套对象字段 | 嵌套对象字段必须添加@Valid |
| 不同场景不同规则 | 分组校验 | 使用@Validated而非@Valid |
| 复杂业务规则 | 自定义校验 | 简单规则优先使用@Pattern |
| 国际化错误消息 | ResourceBundleMessageSource | 创建ValidationMessages.properties文件 |
| 验证集合元素 | @Valid + 集合泛型 | 集合元素必须添加@Valid |
| 忽略某些字段校验 | @JsonIgnore | 适用于JSON序列化 |
| 校验前处理数据 | @InitBinder | 可用于格式化输入数据 |
七、常见验证注解速查
| 注解 | 适用类型 | 常用场景 | 示例 |
|---|---|---|---|
@NotNull | 任意 | ID、关键对象 | @NotNull(message = "ID不能为空") |
@NotBlank | String | 用户名、密码 | @NotBlank(message = "用户名不能为空") |
@NotEmpty | String/Collection/Map/Array | 列表、数组 | @NotEmpty(message = "订单列表不能为空") |
@Size | String/Collection/Map/Array | 长度限制 | @Size(min=6, max=20, message = "密码长度6-20") |
@Email | String | 邮箱验证 | @Email(message = "邮箱格式不正确") |
@Pattern | String | 自定义格式 | @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式错误") |
@Min/@Max | 数字 | 范围限制 | @Min(18) @Max(100) |
@DecimalMin/@DecimalMax | BigDecimal | 金额范围 | @DecimalMin("0.01") @DecimalMax("1000000") |
@Past/@Future | 日期 | 时间验证 | @Past(message = "生日必须是过去的时间") |
