自定义校验注解案例

wen java案例 3

本文目录导读:

自定义校验注解案例

  1. 创建自定义注解
  2. 实现校验器
  3. 更多复杂示例
  4. 使用案例
  5. 全局异常处理
  6. 测试用例
  7. 注意事项

我来给你一个完整的自定义校验注解案例,包括从注解定义到使用的完整流程。

创建自定义注解

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;
}

这个案例涵盖了自定义校验注解的完整实现,从简单的字段校验到复杂的业务校验,配置了全局异常处理,并包含测试用例,你可以根据实际业务需求进行修改和扩展。

抱歉,评论功能暂时关闭!