SpringBoot中接口参数校验与优雅处理的实践教学
作者:(farerboy)

接口参数校验是后端开发的第一道防线。与其在业务代码中写满 if (name == null),不如使用 Spring Boot 提供的参数校验机制,让代码更简洁、逻辑更清晰。
一、 为什么需要参数校验?
在早期的开发中,我们经常在 Service 层写大量的防御性代码:
public void createUser(UserDTO dto) {
if (StringUtils.isEmpty(dto.getName())) {
throw new IllegalArgumentException("用户名不能为空");
}
if (dto.getAge() == null || dto.getAge() < 0) {
throw new IllegalArgumentException("年龄必须为正数");
}
if (!dto.getEmail().matches("^[\\w-\\.]+@([\\w-]+\\.)+[\\w-]{2,4}$")) {
throw new IllegalArgumentException("邮箱格式不正确");
}
// ... 业务逻辑
}
这种方式的缺点显而易见:
- 代码臃肿:校验逻辑与业务逻辑混杂,难以阅读。
- 维护困难:一旦校验规则变更,需要修改多处代码。
- 复用性差:同样的 DTO 在不同接口中可能需要不同的校验规则。
Spring Boot 整合了 Hibernate Validator(实现了 Bean Validation 规范),为我们提供了声明式的参数校验方案。
二、 快速上手
1. 引入依赖
在 Spring Boot 2.3.x 之前,spring-boot-starter-web 已经包含了 validation 依赖。但在 Spring Boot 2.3.x 及之后,需要显式引入:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>2. 定义 DTO 并添加注解
我们在 DTO(Data Transfer Object)的字段上直接添加校验注解:
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在 2-20 之间")
private String username;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄必须大于等于 0")
@Max(value = 150, message = "年龄必须小于等于 150")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
}
3. Controller 层开启校验
在 Controller 的方法参数前添加 @Valid 或 @Validated 注解:
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
public Result<Void> createUser(@Valid @RequestBody UserDTO userDTO) {
// 如果参数校验失败,程序不会进入这里,而是抛出 MethodArgumentNotValidException
userService.createUser(userDTO);
return Result.success();
}
}
三、 进阶用法
1.@Validvs@Validated
@Valid: 属于 JSR-303 标准,支持嵌套校验(即如果一个对象内部包含另一个对象,内部对象的注解也会被校验)。@Validated: 是 Spring 提供的注解,功能更强大,支持分组校验,但默认不支持嵌套校验(需要配合@Valid使用)。
通常在 Controller 参数上使用 @Validated,在 DTO 内部嵌套对象上使用 @Valid。
2. 自定义校验注解
当内置注解无法满足需求时(例如:校验手机号),我们可以自定义注解。
Step 1: 定义注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = PhoneValidator.class) // 指定校验器
public @interface Phone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
Step 2: 实现校验器
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final String PHONE_REGEX = "^1[3-9]\\d{9}$";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (StringUtils.isEmpty(value)) {
return true; // 是否为空由 @NotBlank/@NotNull 控制,这里只校验格式
}
return value.matches(PHONE_REGEX);
}
}
Step 3: 使用
@Data
public class UserDTO {
// ... 其他字段
@Phone(message = "请输入有效的手机号码")
private String phone;
}
3. 分组校验 (Validation Groups)
同一个 DTO 在“创建”和“更新”场景下,校验规则可能不同。例如:创建时 id 应该为空,更新时 id 必须有值。
定义分组接口
public interface CreateGroup {}
public interface UpdateGroup {}
配置 DTO 分组
@Data
public class UserDTO {
@Null(message = "创建用户时 ID 必须为空", groups = CreateGroup.class)
@NotNull(message = "更新用户时 ID 不能为空", groups = UpdateGroup.class)
private Long id;
@NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
private String username;
}
Controller 中指定分组
@PostMapping
public Result<Void> createUser(@Validated(CreateGroup.class) @RequestBody UserDTO userDTO) {
// ...
}
@PutMapping
public Result<Void> updateUser(@Validated(UpdateGroup.class) @RequestBody UserDTO userDTO) {
// ...
}
注意:使用分组校验时,如果字段没有指定 groups,默认属于 Default 组。如果指定了 groups,则 不再属于 Default 组。这意味着如果 Controller 使用 @Validated(不加参数,即 Default 组),那些指定了自定义 Group 的注解将不会生效。
四、 异常处理集成
参数校验失败时,会抛出异常。对于 @RequestBody 参数,抛出的是 MethodArgumentNotValidException;对于 @RequestParam 或路径参数,抛出的是 ConstraintViolationException。
我们需要在全局异常处理器中统一捕获它们:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.fail(400, message);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolation(ConstraintViolationException e) {
String message = e.getConstraintViolations().stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining(", "));
return Result.fail(400, message);
}
}
五、 最佳实践总结
- DTO 与 Entity 分离:校验注解通常加在 DTO 上,不要直接加在数据库实体 Entity 上。
- 快速失败 (Fail-Fast):默认情况下,Validator 会校验所有字段并收集错误。如果希望发现第一个错误就停止,可以在类上添加
@GroupSequence或者自定义 Validator 实现逻辑。 - 明确错误提示:每个校验注解都应该指定清晰的
message,方便前端直接展示给用户。 - 嵌套对象校验:如果 DTO 中包含对象(如
AddressDTO address),需要在字段上同时添加@Valid才能触发内部对象的校验。 - 复杂逻辑校验:对于跨字段校验(如“密码”与“确认密码”必须一致),建议使用自定义 Class 级别的校验注解。
六、 总结
参数校验是系统安全的第一道关卡。通过 Spring Validation 框架,我们将散落在各处的 if-else 转化为声明式的注解,配合全局异常处理,实现了一套既优雅又强大的参数校验方案。掌握这些技巧,让你的接口开发事半功倍!
到此这篇关于SpringBoot中接口参数校验与优雅处理的实践教学的文章就介绍到这了,更多相关SpringBoot接口参数校验与处理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
