本文目录导读:

我来为你详细介绍Java表单校验的规范化流程。
校验分层架构
三层校验体系
Controller层 -> Service层 -> Persistence层
(参数校验) (业务校验) (数据库约束)
标准校验流程
基础校验框架搭建
Maven依赖配置
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 或使用 Hibernate Validator -->
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
Controller层校验(参数校验)
DTO对象定义
@Data
public class UserCreateRequest {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度需要在2-20个字符之间")
private String username;
@NotBlank(message = "密码不能为空")
@Pattern(regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{8,20}$",
message = "密码需包含大小写字母和数字,长度8-20位")
private String password;
@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "年龄不能为空")
@Min(value = 0, message = "年龄不能小于0")
@Max(value = 150, message = "年龄不能大于150")
private Integer age;
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
@Future(message = "过期时间必须是未来时间")
private LocalDateTime expireTime;
}
Controller处理器
@RestController
@Validated
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public Result<Void> createUser(@Valid @RequestBody UserCreateRequest request) {
// 如果参数校验失败,会自动抛出异常
userService.createUser(request);
return Result.success();
}
@GetMapping("/{id}")
public Result<UserVO> getUser(@PathVariable @Min(1) Long id) {
return Result.success(userService.getUser(id));
}
}
分组校验
@Data
public class UserRequest {
@Null(groups = CreateGroup.class)
@NotNull(groups = UpdateGroup.class)
private Long id;
@NotBlank(groups = CreateGroup.class)
private String username;
// 更新时不需要修改密码
@NotBlank(groups = CreateGroup.class)
private String password;
private String email;
// 分组接口
public interface CreateGroup {}
public interface UpdateGroup {}
}
// Controller使用
@PostMapping
public Result<Void> create(@Validated(UserRequest.CreateGroup.class)
@RequestBody UserRequest request) {
userService.create(request);
return Result.success();
}
@PutMapping
public Result<Void> update(@Validated(UserRequest.UpdateGroup.class)
@RequestBody UserRequest request) {
userService.update(request);
return Result.success();
}
Service层校验(业务校验)
@Service
@Validated
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Override
public void createUser(UserCreateRequest request) {
// 1. 参数校验(由Controller层保证)
// 2. 业务校验
validateBusinessRules(request);
// 3. 转换为实体
User user = new User();
BeanUtils.copyProperties(request, user);
// 4. 持久化
userRepository.save(user);
}
private void validateBusinessRules(UserCreateRequest request) {
// 用户名唯一性校验
if (userRepository.existsByUsername(request.getUsername())) {
throw new BusinessException(ErrorCode.USERNAME_EXISTS, "用户名已存在");
}
// 邮箱唯一性校验
if (userRepository.existsByEmail(request.getEmail())) {
throw new BusinessException(ErrorCode.EMAIL_EXISTS, "邮箱已被注册");
}
// 复杂度校验
if (isPasswordWeak(request.getPassword())) {
throw new BusinessException(ErrorCode.PASSWORD_WEAK, "密码强度不足");
}
}
private boolean isPasswordWeak(String password) {
// 实现密码强度检查逻辑
return false;
}
}
自定义校验注解
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface ValidPhone {
String message() default "手机号格式不正确";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class PhoneValidator implements ConstraintValidator<ValidPhone, String> {
private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
if (phone == null) {
return true; // 由@NotBlank处理空值
}
return PHONE_PATTERN.matcher(phone).matches();
}
}
// 使用方式
@ValidPhone
private String phone;
全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidationException(MethodArgumentNotValidException ex) {
// 获取第一个错误信息
String message = ex.getBindingResult()
.getAllErrors()
.stream()
.findFirst()
.map(error -> error.getDefaultMessage())
.orElse("参数校验失败");
return Result.error(400, message);
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<Void> handleConstraintViolation(ConstraintViolationException ex) {
String message = ex.getConstraintViolations()
.stream()
.findFirst()
.map(violation -> violation.getMessage())
.orElse("参数校验失败");
return Result.error(400, message);
}
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException ex) {
return Result.error(ex.getCode(), ex.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception ex) {
log.error("系统异常", ex);
return Result.error(500, "系统繁忙,请稍后重试");
}
}
校验规范建议
校验粒度控制
// ✅ 好的实践:明确的校验级别
public class OrderCreateRequest {
@NotNull
private Long productId; // 必须校验
private String couponCode; // 可选字段
public boolean hasCoupon() {
return couponCode != null && !couponCode.trim().isEmpty();
}
}
统一返回格式
@Data
public class Result<T> {
private int code;
private String message;
private T data;
private long timestamp;
public static <T> Result<T> success() {
return success(null);
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.code = 200;
result.message = "success";
result.data = data;
result.timestamp = System.currentTimeMillis();
return result;
}
public static <T> Result<T> error(int code, String message) {
Result<T> result = new Result<>();
result.code = code;
result.message = message;
result.timestamp = System.currentTimeMillis();
return result;
}
}
校验优先级示例
// 优先使用框架提供的注解
@NotEmpty(message = "订单号不能为空")
@Size(max = 32, message = "订单号长度不能超过32位")
private String orderNo;
// 复杂的业务逻辑使用自定义注解或方法校验
@ValidOrderStatus
private Integer status;
// 跨字段校验(如密码确认)
public class PasswordChangeRequest {
@NotBlank
private String newPassword;
@NotBlank
private String confirmPassword;
@AssertTrue(message = "两次输入的密码不一致")
public boolean isPasswordMatch() {
return newPassword != null && newPassword.equals(confirmPassword);
}
}
性能优化建议
// 使用正则表达式预编译
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)$");
// 批量校验优化
public void batchValidate(List<UserCreateRequest> requests) {
List<String> errors = new ArrayList<>();
requests.parallelStream().forEach(request -> {
try {
validateSingle(request);
} catch (ValidationException e) {
synchronized (errors) {
errors.add("第" + index + "条数据: " + e.getMessage());
}
}
});
if (!errors.isEmpty()) {
throw new ValidationException(String.join("; ", errors));
}
}
测试用例示例
@SpringBootTest
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testCreateUserValidation() throws Exception {
String requestBody = """
{
"username": "",
"email": "invalid-email",
"password": "123"
}
""";
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(requestBody))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").value("用户名不能为空"));
}
}
最佳实践总结
- 分层校验:Controller负责参数格式,Service负责业务逻辑
- 分组校验:根据不同场景使用不同校验规则
- 自定义注解:封装复杂校验逻辑,提高复用性
- 统一异常处理:减少重复代码,统一错误格式
- 清晰的错误信息:提供具体的错误描述,便于定位问题
- 性能考虑:避免在循环中重复创建对象,预编译正则表达式
- 安全性:避免暴露敏感信息(如SQL细节、系统路径等)
这样的校验流程能保证代码的可维护性、可读性和健壮性,同时提供良好的用户体验。