Springboot入参校验实战:使用 javax.validation 优雅处理参数校验

背景

在日常开发中经常需要处理各种参数校验。传统的 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() 会自动返回当前语言的消息,无需你手动处理国际化


至此,本文分享到此结束!!!

相关推荐
烤麻辣烫2 小时前
黑马程序员苍穹外卖(新手) DAY3
java·开发语言·spring boot·学习·intellij-idea
百***49003 小时前
基于SpringBoot和PostGIS的各省与地级市空间距离分析
java·spring boot·spring
爱分享的鱼鱼4 小时前
Srpingboot入门:通过实践项目系统性理解Springboot框架
spring boot·后端·spring
wsaaaqqq4 小时前
springboot加载外部jar
spring boot
q***21604 小时前
【监控】spring actuator源码速读
java·spring boot·spring
原来是好奇心4 小时前
Spring AI 入门实战:快速构建智能 Spring Boot 应用
人工智能·spring boot·spring
wa的一声哭了5 小时前
WeBASE管理平台部署-WeBASE-Web
linux·前端·网络·arm开发·spring boot·架构·区块链
WX-bisheyuange6 小时前
基于Spring Boot的老年人的景区订票系统
vue.js·spring boot·后端·毕业设计
ArabySide6 小时前
【Spring Boot】基于MyBatis的条件分页
java·spring boot·后端·mybatis