本文目录导读:

- 手动校验(最基础,无依赖)
- Java Bean Validation(JSR 380 / jakarta.validation)
- 方法级别的参数校验(非RequestBody)
- 自定义校验注解
- 参数校验的最佳实践总结
在Java中实现参数校验有多种方式,从简单的手动校验到使用框架注解,从基础类型到复杂对象,每种方式都有其适用场景。
以下是主流的几种实现方案,由浅入深:
手动校验(最基础,无依赖)
适合简单场景或不需要引入额外框架的情况。
示例代码:
public class UserService {
public void register(String username, String password, Integer age) {
// 基本非空判断
if (username == null || username.trim().isEmpty()) {
throw new IllegalArgumentException("用户名不能为空");
}
// 长度校验
if (username.length() < 3 || username.length() > 20) {
throw new IllegalArgumentException("用户名长度必须在3-20之间");
}
if (password == null || password.length() < 6) {
throw new IllegalArgumentException("密码长度不能少于6位");
}
if (age == null || age < 0 || age > 150) {
throw new IllegalArgumentException("年龄不合法");
}
// ... 业务逻辑
}
}
优点: 简单直接,无任何依赖。
缺点: 代码冗余,重复性强,可维护性差。
Java Bean Validation(JSR 380 / jakarta.validation)
这是目前最主流、最推荐的方式,通常配合 Hibernate Validator(实现)。
1 添加依赖(Maven示例)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<!-- Spring Boot会自动引入Hibernate Validator -->
</dependency>
2 在实体/参数对象上使用注解
import jakarta.validation.constraints.*;
import jakarta.validation.constraints.NotNull;
public class UserRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 3, max = 20, message = "用户名长度必须在3-20之间")
private String username;
@NotBlank(message = "密码不能为空")
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能超过150")
private Integer age;
@Email(message = "邮箱格式不正确")
private String email;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
// getters and setters...
}
3 在Controller中启用校验
@Valid + 自动抛出异常(推荐)
@RestController
@Validated // 可选,用于类级别的方法参数校验
public class UserController {
@PostMapping("/register")
public Result register(@Valid @RequestBody UserRequest request) {
// 如果校验失败,Spring会自动抛出MethodArgumentNotValidException
// 返回400 Bad Request,并包含具体错误信息
return Result.success(userService.register(request));
}
}
手动捕获校验结果
@PostMapping("/register")
public Result register(@Valid @RequestBody UserRequest request, BindingResult result) {
if (result.hasErrors()) {
// 收集所有错误信息
List<String> errors = result.getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.toList());
return Result.error(400, "参数校验失败: " + String.join(", ", errors));
}
return Result.success(userService.register(request));
}
4 统一异常处理(优雅返回)
通常配合 @RestControllerAdvice 统一处理校验异常,避免在每个Controller里重复处理。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result handleValidationExceptions(MethodArgumentNotValidException ex) {
String errorMessage = ex.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(400, "参数校验失败: " + errorMessage);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result handleConstraintViolation(ConstraintViolationException ex) {
String errorMessage = ex.getConstraintViolations().stream()
.map(cv -> cv.getPropertyPath() + ": " + cv.getMessage())
.collect(Collectors.joining(", "));
return Result.error(400, "参数校验失败: " + errorMessage);
}
}
方法级别的参数校验(非RequestBody)
适用于 @RequestParam、@PathVariable 等非对象参数。
@RestController
@Validated // 类上必须加 @Validated
public class UserController {
@GetMapping("/users/{id}")
public Result getUser(@PathVariable @Min(1) Long id) {
// ...
}
@GetMapping("/search")
public Result search(
@RequestParam @NotBlank String keyword,
@RequestParam @Min(1) @Max(100) int pageSize) {
// ...
}
}
注意: 这里如果校验失败,抛出的异常是 ConstraintViolationException,需要在全局异常处理器中捕获(见上述代码)。
自定义校验注解
当内置注解(如 @NotNull, @Size)不满足复杂业务需求时,可以自定义。
1 创建自定义注解
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;
@Documented
@Constraint(validatedBy = PhoneValidator.class) // 指定校验器
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidPhone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2 实现校验器
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
private static final String PHONE_PATTERN = "^1[3-9]\\d{9}$";
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null || value.isEmpty()) {
return true; // 非空校验由 @NotNull 负责
}
return value.matches(PHONE_PATTERN);
}
}
3 使用自定义注解
public class UserRequest {
@ValidPhone // 使用自定义注解
private String phone;
}
参数校验的最佳实践总结
| 场景 | 推荐方案 |
|---|---|
| 简单对象(如接收JSON的DTO) | @Valid + 注解 |
| 单个参数(如URL参数、路径变量) | @Validated(类级) + 注解 |
| 复杂业务逻辑校验 | 自定义注解 + 校验器 |
| 跨字段校验(如密码和确认密码一致) | 自定义类级别注解(@Target(TYPE))或手动校验 |
| 性能要求极高、避免反射 | 手动校验(牺牲开发效率) |
| 需要国际化错误信息 | 在 ValidationMessages.properties 中配置 |
最终建议:
- 首选 Jakarta Validation(
@Valid+ 注解) - 配合 全局异常处理 统一返回格式
- 对复杂业务逻辑使用 自定义注解 或 Service层手动校验
- 避免在Controller中写大量
if-else校验代码