0Pricing
Java Academy · 课时

自定义约束注解

创建自定义 @UniqueEmail 注解,并实现 ConstraintValidator

自定义约束注解 是 CoddyKit 上的免费 Java Academy 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Java Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Java Academy 课程共包含 4 节课。

标准约束不足时

标准 Bean 验证注解可以覆盖常见场景。对于特定于领域的规则(例如唯一电子邮件、有效 IBAN 和密码强度),请创建自定义约束注解。

定义约束注解

使用 @Constraint(validatedBy = ...) 创建注解,并提供必需的消息、分组和负载元素,以及适当的保留策略和目标。

import jakarta.validation.Constraint;
import jakarta.validation.Payload;
@Documented
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
    String message() default "Email already registered";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

实现 ConstraintValidator

实现 ConstraintValidator<UniqueEmail, String>。初始化方法读取注解元素;isValid 执行实际检查。

@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    @Autowired private UserRepository repo;
    @Override
    public boolean isValid(String email, ConstraintValidatorContext ctx) {
        if (email == null) return true; // let @NotBlank handle null
        return !repo.existsByEmail(email);
    }
}

使用自定义约束

像使用标准约束一样,将该注解应用于字段、参数或返回值。

public record CreateUserRequest(
    @NotBlank
    String name,
    @NotBlank @Email @UniqueEmail
    String email
) {}

使用插值生成自定义消息

使用 {elementName} 在消息中引用注解元素。使用 ctx.buildConstraintViolationWithTemplate(),通过动态数据添加自定义消息。

@Constraint(validatedBy = RangeValidator.class)
public @interface InRange {
    int min() default 0;
    int max() default 100;
    String message() default "Must be between {min} and {max}";
    // ...
}

类级别约束

在类级别应用约束,以便同时验证多个字段,这对于密码确认等跨字段规则非常有用。

@Documented
@Constraint(validatedBy = PasswordMatchValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordMatch {
    String message() default "Passwords do not match";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

类级别验证器实现

验证器会接收整个对象。访问多个字段即可执行跨字段验证。

public class PasswordMatchValidator implements ConstraintValidator<PasswordMatch, ChangePasswordRequest> {
    public boolean isValid(ChangePasswordRequest req, ConstraintValidatorContext ctx) {
        if (req.getNewPassword() == null) return true;
        boolean match = req.getNewPassword().equals(req.getConfirmPassword());
        if (!match) {
            ctx.disableDefaultConstraintViolation();
            ctx.buildConstraintViolationWithTemplate("Passwords do not match")
               .addPropertyNode("confirmPassword").addConstraintViolation();
        }
        return match;
    }
}

组合约束

通过在自定义注解上添加现有约束来创建元约束。两者都会自动应用。

@NotBlank
@Email
@Size(max = 255)
@Constraint(validatedBy = {})
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidEmail {
    String message() default "Invalid email";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

返回多个违规项

使用 ctx.disableDefaultConstraintViolation(),在一次验证器执行过程中为发现的每个问题添加单独的违规消息。

public boolean isValid(String password, ConstraintValidatorContext ctx) {
    List<String> issues = new ArrayList<>();
    if (password.length() < 8) issues.add("Too short");
    if (!password.matches(".*\\d.*")) issues.add("Must contain a digit");
    if (issues.isEmpty()) return true;
    ctx.disableDefaultConstraintViolation();
    issues.forEach(msg -> ctx.buildConstraintViolationWithTemplate(msg).addConstraintViolation());
    return false;
}

由 Spring 管理的验证器

由于 UniqueEmailValidator 使用 @Component 注解,Spring 会向其中注入仓储。只有通过 Spring 的验证器引导该验证器时才有效;Spring Boot 会自动完成此配置。

测试自定义验证器

直接对验证器进行单元测试,不需要 Spring 上下文即可测试纯验证逻辑。请使用 Validation.buildDefaultValidatorFactory() 进行集成测试。

Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
CreateUserRequest req = new CreateUserRequest("", "bad-email", null);
Set<ConstraintViolation<CreateUserRequest>> violations = validator.validate(req);
System.out.println(violations.size()); // 3

快速检查

自定义约束验证器实现哪个接口?

回顾

使用 @interface 和 @Constraint(validatedBy=...) 创建注解。实现 ConstraintValidator。使用 @Component 进行 Spring 注入。类级别约束可以同时验证多个字段。组合现有约束以避免重复。

常见问题解答

「自定义约束注解」课时是免费的吗?

是的 — 「自定义约束注解」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Java Academy 课程的其余内容,请升级到 CoddyKit PRO。 Java Academy 课程共包含 4 节课。

「自定义约束注解」这节课中我会学到什么?

创建自定义 @UniqueEmail 注解,并实现 ConstraintValidator 你通过在浏览器中直接运行的动手代码来练习 Java Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Java Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Java Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。

「自定义约束注解」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Java Academy 课中编写并运行代码吗?

能。每节 Java Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. Bean 验证:@NotNull、@Size、@Pattern
  2. 自定义约束注解
  3. 使用 @ControllerAdvice 进行全局异常处理
  4. RFC 7807 问题详情与一致的错误响应
← 返回 Java Academy