本文目录导读:

我来给你一个完整的自定义校验注解案例,包括从注解定义到使用的完整流程。
创建自定义注解
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
/**
* 手机号校验注解
*/
@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
// 默认错误消息
String message() default "手机号格式不正确";
// 分组
Class<?>[] groups() default {};
// 负载
Class<? extends Payload>[] payload() default {};
}
实现校验器
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Pattern;
/**
* 手机号校验器
*/
public class PhoneValidator implements ConstraintValidator<Phone, String> {
// 简单的手机号正则
private static final Pattern PHONE_PATTERN = Pattern.compile(
"^1[3-9]\\d{9}$"
);
@Override
public void initialize(Phone constraintAnnotation) {
// 初始化方法,可以获取注解参数
}
@Override
public boolean isValid(String phone, ConstraintValidatorContext context) {
if (phone == null || phone.isEmpty()) {
// 是否允许为空由 @NotNull 等注解控制
return true;
}
return PHONE_PATTERN.matcher(phone).matches();
}
}
更多复杂示例
1 带参数的校验注解
/**
* 金额范围校验注解
*/
@Documented
@Constraint(validatedBy = MoneyRangeValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MoneyRange {
double min() default 0;
double max() default Double.MAX_VALUE;
String message() default "金额超出范围";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
2 复杂校验器实现
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.math.BigDecimal;
/**
* 金额范围校验器
*/
public class MoneyRangeValidator implements ConstraintValidator<MoneyRange, BigDecimal> {
private BigDecimal min;
private BigDecimal max;
@Override
public void initialize(MoneyRange constraintAnnotation) {
this.min = BigDecimal.valueOf(constraintAnnotation.min());
this.max = BigDecimal.valueOf(constraintAnnotation.max());
}
@Override
public boolean isValid(BigDecimal value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
boolean isValid = value.compareTo(min) >= 0 && value.compareTo(max) <= 0;
if (!isValid) {
// 自定义错误消息
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
String.format("金额必须在 %.2f 到 %.2f 之间", min, max)
).addConstraintViolation();
}
return isValid;
}
}
3 对象级联校验注解
/**
* 密码强度校验注解
*/
@Documented
@Constraint(validatedBy = PasswordValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Password {
int minLength() default 8;
int maxLength() default 20;
boolean requireUpperCase() default true;
boolean requireLowerCase() default true;
boolean requireDigit() default true;
String message() default "密码强度不足";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
/**
* 密码强度校验器
*/
public class PasswordValidator implements ConstraintValidator<Password, String> {
private Password password;
@Override
public void initialize(Password constraintAnnotation) {
this.password = constraintAnnotation;
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true;
}
// 长度检查
if (value.length() < password.minLength() || value.length() > password.maxLength()) {
return false;
}
// 字符类型检查
if (password.requireUpperCase() && !value.matches(".*[A-Z].*")) {
return false;
}
if (password.requireLowerCase() && !value.matches(".*[a-z].*")) {
return false;
}
if (password.requireDigit() && !value.matches(".*\\d.*")) {
return false;
}
return true;
}
}
使用案例
1 在实体类中使用
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
public class UserDTO {
@NotBlank(message = "用户名不能为空")
private String username;
@Phone(message = "手机号格式不正确")
private String phone;
@Password(message = "密码至少8位,需包含大写字母、小写字母和数字")
private String password;
@MoneyRange(min = 100, max = 10000, message = "账户余额必须在100-10000之间")
private BigDecimal balance;
// getter/setter 省略
}
2 Spring Boot Controller中使用
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {
/**
* 创建用户
*/
@PostMapping
public Result createUser(@Valid @RequestBody UserDTO userDTO) {
// 参数已经通过校验
return Result.success(userService.createUser(userDTO));
}
/**
* 查询用户(参数校验)
*/
@GetMapping("/{phone}")
public Result getUserByPhone(@Phone(message = "手机号格式错误") @PathVariable String phone) {
return Result.success(userService.getUserByPhone(phone));
}
/**
* 查询用户(RequestParam校验)
*/
@GetMapping("/query")
public Result queryUsers(
@Phone(message = "手机号格式错误")
@RequestParam(required = false) String phone,
@MoneyRange(min = 0, max = 5000, message = "余额范围错误")
@RequestParam(required = false) BigDecimal balance) {
return Result.success(userService.queryUsers(phone, balance));
}
}
3 Service层使用
import org.springframework.validation.annotation.Validated;
import javax.validation.Valid;
@Service
@Validated
public class UserServiceImpl implements UserService {
@Override
public User createUser(@Valid @RequestBody UserDTO userDTO) {
// 校验通过后执行业务逻辑
return userMapper.insert(userDTO);
}
/**
* 更新用户信息
*/
@Override
public User updateUser(@Valid UserDTO userDTO) {
// 可以指定分组进行部分校验
return userMapper.update(userDTO);
}
}
全局异常处理
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理请求体校验异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleMethodArgumentNotValid(
MethodArgumentNotValidException ex) {
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage,
(existing, replacement) -> existing
));
Map<String, Object> response = new HashMap<>();
response.put("code", 400);
response.put("message", "请求参数校验失败");
response.put("errors", errors);
return ResponseEntity.badRequest().body(response);
}
/**
* 处理参数校验异常(@RequestParam等)
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, Object>> handleConstraintViolation(
ConstraintViolationException ex) {
String message = ex.getConstraintViolations()
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.joining("; "));
Map<String, Object> response = new HashMap<>();
response.put("code", 400);
response.put("message", message);
return ResponseEntity.badRequest().body(response);
}
/**
* 处理表单绑定异常
*/
@ExceptionHandler(BindException.class)
public ResponseEntity<Map<String, Object>> handleBindException(BindException ex) {
Map<String, String> errors = ex.getBindingResult()
.getFieldErrors()
.stream()
.collect(Collectors.toMap(
FieldError::getField,
FieldError::getDefaultMessage
));
Map<String, Object> response = new HashMap<>();
response.put("code", 400);
response.put("message", "参数绑定失败");
response.put("errors", errors);
return ResponseEntity.badRequest().body(response);
}
}
测试用例
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import javax.validation.ConstraintViolation;
import javax.validation.Validator;
import java.math.BigDecimal;
import java.util.Set;
@RunWith(SpringRunner.class)
@SpringBootTest
public class ValidationTest {
@Autowired
private Validator validator;
@Test
public void testPhoneValidation() {
UserDTO user = new UserDTO();
user.setPhone("13800138000"); // 有效手机号
Set<ConstraintViolation<UserDTO>> violations = validator.validate(user);
assert violation.isEmpty();
}
@Test
public void testInvalidPhone() {
UserDTO user = new UserDTO();
user.setPhone("12345"); // 无效手机号
Set<ConstraintViolation<UserDTO>> violations = validator.validate(user);
assert !violations.isEmpty();
assert violations.iterator().next().getMessage().equals("手机号格式不正确");
}
@Test
public void testPasswordValidation() {
UserDTO user = new UserDTO();
user.setPassword("Password123"); // 符合要求
Set<ConstraintViolation<UserDTO>> violations = validator.validate(user);
assert violation.isEmpty();
}
@Test
public void testMoneyRange() {
UserDTO user = new UserDTO();
user.setBalance(new BigDecimal("150.00")); // 在100-10000之间
Set<ConstraintViolation<UserDTO>> violations = validator.validate(user);
assert violation.isEmpty();
user.setBalance(new BigDecimal("50.00")); // 超出范围
violations = validator.validate(user);
assert !violations.isEmpty();
}
}
注意事项
// 1. 需要导入相关依赖
// Maven
/*
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
*/
// 2. 启动类需要添加@Validated注解
@SpringBootApplication
@Validated
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
// 3. 分组校验
public interface CreateGroup {}
public interface UpdateGroup {}
public class UserDTO {
@Null(groups = CreateGroup.class, message = "创建时ID必须为空")
@NotNull(groups = UpdateGroup.class, message = "更新时ID不能为空")
private Long id;
@NotBlank(groups = CreateGroup.class, message = "创建时用户名不能为空")
private String username;
}
这个案例涵盖了自定义校验注解的完整实现,从简单的字段校验到复杂的业务校验,配置了全局异常处理,并包含测试用例,你可以根据实际业务需求进行修改和扩展。