背景
在日常开发中经常需要处理各种参数校验。传统的 if-else 判断方式不仅代码冗余,而且可读性差。今天我们来聊聊如何在 SpringBoot 中使用 javax.validation 优雅地实现入参校验。
为什么需要参数校验?
在 Web 开发中,参数校验是保证系统安全性和数据完整性的第一道防线。合理的参数校验能够:
- 防止恶意数据攻击
- 保证业务逻辑的正确执行
- 提供清晰的错误提示信息
- 减少不必要的数据库查询
环境准备
首先确保你的 SpringBoot 项目中已经包含了 validation 依赖:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
常用校验注解
javax.validation 提供了一系列强大的校验注解:
| 注解 | 说明 |
|---|---|
@NotNull |
值不能为 null |
@NotEmpty |
值不能为 null 或空字符串/集合 |
@NotBlank |
值不能为 null 或空白字符 |
@Size |
字符串/集合大小必须在指定范围内 |
@Min |
数字最小值 |
@Max |
数字最大值 |
@Email |
必须为邮箱格式 |
@Pattern |
必须匹配正则表达式 |
实战演示
定义校验实体
java
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
private String password;
@Email(message = "邮箱格式不正确")
@NotBlank(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;
}
Controller 层使用校验
java
@RestController
@RequestMapping("/api/user")
@Validated
public class UserController {
@PostMapping("/register")
public ResponseEntity<Result<?>> register(@Valid @RequestBody UserDTO userDTO) {
// 业务处理逻辑
return ResponseEntity.ok(Result.success("注册成功"));
}
@GetMapping("/detail")
public ResponseEntity<Result<?>> getUserDetail(
@NotBlank(message = "用户ID不能为空")
@RequestParam String userId) {
// 查询用户详情逻辑
return ResponseEntity.ok(Result.success("查询成功"));
}
}
自定义校验注解
当内置注解不能满足需求时,我们可以自定义校验注解:
java
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EnumValueValidator.class)
public @interface EnumValue {
String message() default "参数值不在可选范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String[] values() default {};
}
校验器实现:
java
public class EnumValueValidator implements ConstraintValidator<EnumValue, String> {
private Set<String> allowedValues;
@Override
public void initialize(EnumValue constraintAnnotation) {
allowedValues = Arrays.stream(constraintAnnotation.values())
.collect(Collectors.toSet());
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
return value == null || allowedValues.contains(value);
}
}
使用自定义注解:
java
public class UserDTO {
// ... 其他字段
@EnumValue(values = {"male", "female"}, message = "性别只能是male或female")
private String gender;
}
全局异常处理
全局异常捕获类
java
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理表单参数校验异常
*/
@ExceptionHandler(BindException.class)
public BaseResponse<String> bindExceptionHandler(BindException exception) {
log.error("bindExceptionHandler: ", exception);
return BaseResponse.error(ResStatus.PARAM_ERROR.getCode(), exception.getMessage());
}
/**
* 处理JSON参数校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public BaseResponse<String> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException exception) {
log.warn("methodArgumentNotValidExceptionHandle: ", exception);
StringBuilder sb = new StringBuilder();
exception.getBindingResult().getFieldErrors()
.forEach(error-> sb.append(error.getField())
.append(":")
.append(error.getDefaultMessage())
.append("; "));
return BaseResponse.error(ResStatus.PARAM_ERROR.getCode(), sb.toString());
}
/**
* 处理单个参数校验异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public BaseResponse<String> constraintViolationExceptionHandler(ConstraintViolationException exception) {
log.error("constraintViolationExceptionHandler: ", exception);
return BaseResponse.error(ResStatus.PARAM_ERROR.getCode(), exception.getMessage());
}
}
统一返回结果类
java
import cn.com.xxx.BusinessErrorConstant;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 响应包的基类
*/
@Data
@NoArgsConstructor
public class BaseResponse<T> {
private int status;
private String errCode;
private String msg;
private T data;
private BaseResponse(int status, String errCode, String msg, T data) {
this.status = status;
this.errCode = errCode;
this.msg = msg;
this.data = data;
}
public static <T> BaseResponse<T> error(BusinessErrorConstant businessErrorConstant, Object... args) {
return new BaseResponse<>(businessErrorConstant.getStatusCode(), businessErrorConstant.getErrCode(),
BusinessErrorConstant.getErrMsg(businessErrorConstant, args), null);
}
}
分组校验
使用场景:在不同场景下,我们可能需要对同一个实体进行不同的校验规则。
定义分组
java
/**
* <p>
* 入参校验分组
* </p>
*/
public interface Groups {
interface CreateGroup {}
interface UpdateGroup {}
}
使用分组
java
@Data
public class UserDTO {
@NotNull(message = "用户ID不能为空", groups = Groups.UpdateGroup.class)
private Long id;
@NotBlank(message = "用户名不能为空", groups = {Groups.CreateGroup.class, UpdateGroup.class})
private String username;
@NotBlank(message = "密码不能为空", groups = Groups.CreateGroup.class)
private String password;
// 如果不指定分组,则会放在默认分组Default.class下
@Email(message = "邮箱格式不正确")
@NotBlank(message = "邮箱不能为空")
private String email;
}
在 Controller 中使用分组校验:
java
@PostMapping("/create")
public BaseResponse<String> createUser(@Validated(Groups.CreateGroup.class) @RequestBody UserDTO userDTO) {
// 创建用户逻辑
return BaseResponse.ok("创建成功");
}
@PutMapping("/update")
public BaseResponse<String> updateUser(@Validated(Groups.UpdateGroup.class) @RequestBody UserDTO userDTO) {
// 更新用户逻辑
return BaseResponse.ok("更新成功");
}
注意:如果字段没有指定分组(即Default.class)分组下,入参校验不会校验相关的属性,如果想要校验默认分组字段,可以将Default分组也加入到校验规则中,修改如下:
java
@PostMapping("/create")
public BaseResponse<String> createUser(@Validated({Groups.CreateGroup.class, Default.class}) @RequestBody UserDTO userDTO) {
// 创建用户逻辑
return BaseResponse.ok("创建成功");
}
@PutMapping("/update")
public BaseResponse<String> updateUser(@Validated({Groups.UpdateGroup.class, Default.class}) @RequestBody UserDTO userDTO) {
// 更新用户逻辑
return BaseResponse.ok("更新成功");
}
嵌套对象校验
对于复杂的嵌套对象,我们也可以进行校验:
java
@Data
public class OrderDTO {
@NotBlank(message = "订单号不能为空")
private String orderNo;
@Valid
@NotNull(message = "用户信息不能为空")
private UserDTO user;
@Valid
@NotEmpty(message = "订单项不能为空")
private List<OrderItemDTO> items;
}
@Data
public class OrderItemDTO {
@NotBlank(message = "商品ID不能为空")
private String productId;
@Min(value = 1, message = "商品数量必须大于0")
private Integer quantity;
}
国际化实现
在 Spring Boot 中,javax.validation(Bean Validation) 的国际化支持是 "自动的" ,只要你按照规范命名资源文件,并 让 Spring MVC 知道你当前使用哪种语言(Locale) 即可。
✅ 第一步:创建多语言资源文件
在 src/main/resources/ 下创建如下文件:
ValidationMessages_zh_CN.properties # 简体中文
ValidationMessages_en_US.properties # 美式英文
注意:文件名必须是
ValidationMessages+ 语言代码,这是 Bean Validation 规范要求的,不能改名字。
✅ 第二步:在资源文件中定义消息
✅ 目录结构参考
src/main/resources/
├── ValidationMessages.properties
├── ValidationMessages_en.properties
├── ValidationMessages_zh_CN.properties
但是我放在 src/main/resources/i18n 下也没有问题,可能跟Springboot配置文件加载顺序有关。
ValidationMessages.properties
properties
javax.validation.constraints.Null.message=Field must be null
ValidationMessages_zh_CN.properties
properties
# 基础约束
javax.validation.constraints.NotNull.message=不能为 null
javax.validation.constraints.Null.message=必须为 null
javax.validation.constraints.AssertTrue.message=必须为 true
javax.validation.constraints.AssertFalse.message=必须为 false
# 字符串约束
javax.validation.constraints.NotEmpty.message=不能为空字符串
javax.validation.constraints.NotBlank.message=不能为空白字符串
javax.validation.constraints.Size.message=长度必须在 {min} 和 {max} 之间
javax.validation.constraints.Pattern.message=必须匹配正则表达式: {regexp}
# 数值约束
javax.validation.constraints.Min.message=必须大于或等于 {value}
javax.validation.constraints.Max.message=必须小于或等于 {value}
javax.validation.constraints.DecimalMin.message=必须大于或等于 {value}
javax.validation.constraints.DecimalMax.message=必须小于或等于 {value}
javax.validation.constraints.Digits.message=数字超出范围 (应为 <{integer} 位整数>.<{fraction} 位小数>)
javax.validation.constraints.Negative.message=必须小于 0
javax.validation.constraints.NegativeOrZero.message=必须小于或等于 0
javax.validation.constraints.Positive.message=必须大于 0
javax.validation.constraints.PositiveOrZero.message=必须大于或等于 0
# 特殊格式约束
javax.validation.constraints.Email.message=邮箱地址格式不正确
javax.validation.constraints.Future.message=必须是将来的日期
javax.validation.constraints.FutureOrPresent.message=必须是将来或现在的日期
javax.validation.constraints.Past.message=必须是过去的日期
javax.validation.constraints.PastOrPresent.message=必须是过去或现在的日期
# Hibernate Validator 特有约束(如果使用 Hibernate Validator)
org.hibernate.validator.constraints.Length.message=长度必须在 {min} 和 {max} 之间
org.hibernate.validator.constraints.Range.message=必须在 {min} 和 {max} 之间
org.hibernate.validator.constraints.URL.message=必须是有效的 URL
org.hibernate.validator.constraints.CreditCardNumber.message=无效的信用卡号
org.hibernate.validator.constraints.Currency.message=必须是有效的货币
org.hibernate.validator.constraints.UniqueElements.message=必须包含唯一元素
org.hibernate.validator.constraints.CodePointLength.message=代码点长度必须在 {min} 和 {max} 之间
org.hibernate.validator.constraints.br.CPF.message=无效的巴西 CPF
org.hibernate.validator.constraints.br.CNPJ.message=无效的巴西 CNPJ
# 自定义验证消息(示例)
custom.validation.phone.message=手机号码格式不正确
custom.validation.zipcode.message=邮政编码格式不正确
custom.validation.idcard.message=身份证号码格式不正确
ValidationMessages_en_US.properties
properties
# 基础约束
javax.validation.constraints.NotNull.message=Cannot be null
javax.validation.constraints.Null.message=Must be null
javax.validation.constraints.AssertTrue.message=Must be true
javax.validation.constraints.AssertFalse.message=Must be false
# 字符串约束
javax.validation.constraints.NotEmpty.message=Cannot be an empty string
javax.validation.constraints.NotBlank.message=Cannot be blank
javax.validation.constraints.Size.message=The length must be between {min} and {max}
javax.validation.constraints.Pattern.message=Must match the regular expression: {regexp}
# 数值约束
javax.validation.constraints.Min.message=Must be greater than or equal to {value}
javax.validation.constraints.Max.message=Must be less than or equal to {value}
javax.validation.constraints.DecimalMin.message=Must be greater than or equal to {value}
javax.validation.constraints.DecimalMax.message=Must be less than or equal to {value}
javax.validation.constraints.Digits.message=Numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected)
javax.validation.constraints.Negative.message=Must be less than 0
javax.validation.constraints.NegativeOrZero.message=Must be less than or equal to 0
javax.validation.constraints.Positive.message=Must be greater than 0
javax.validation.constraints.PositiveOrZero.message=Must be greater than or equal to 0
# 特殊格式约束
javax.validation.constraints.Email.message=The email address format is incorrect
javax.validation.constraints.Future.message=Must be a future date
javax.validation.constraints.FutureOrPresent.message=Must be a future or present date
javax.validation.constraints.Past.message=Must be a past date
javax.validation.constraints.PastOrPresent.message=Must be a past or present date
# Hibernate Validator 特有约束(如果使用 Hibernate Validator)
org.hibernate.validator.constraints.Length.message=Length must be between {min} and {max}
org.hibernate.validator.constraints.Range.message=Must be between {min} and {max}
org.hibernate.validator.constraints.URL.message=Must be a valid URL
org.hibernate.validator.constraints.CreditCardNumber.message=Invalid credit card number
org.hibernate.validator.constraints.Currency.message=Must be a valid currency
org.hibernate.validator.constraints.UniqueElements.message=Must contain unique elements
org.hibernate.validator.constraints.CodePointLength.message=Code point length must be between {min} and {max}
org.hibernate.validator.constraints.br.CPF.message=Invalid Brazilian CPF
org.hibernate.validator.constraints.br.CNPJ.message=Invalid Brazilian CNPJ
✅ 第三步:去掉实体类自定义message
java
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20)
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20)
private String password;
@Email
@NotBlank
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$")
private String phone;
@Min(value = 18)
@Max(value = 100)
private Integer age;
}
✅ 第四步:让 Spring 知道你当前使用哪种语言(Locale)
Spring Boot 默认使用 Accept-Language 请求头来决定当前语言。
示例请求:
bash
# 中文
curl -X POST http://localhost:8080/user \
-H "Accept-Language: zh-CN" \
-H "Content-Type: application/json" \
-d '{"name": "abc"}'
# 英文
curl -X POST http://localhost:8080/user \
-H "Accept-Language: en-US" \
-H "Content-Type: application/json" \
-d '{"name": "abc"}'
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(
MethodArgumentNotValidException ex, HttpServletRequest request) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
return ResponseEntity.badRequest().body(errors);
}
}
PS:全局异常捕获内容补充
error.getDefaultMessage() 会自动返回当前语言的消息,无需你手动处理国际化。
至此,本文分享到此结束!!!