自定义跨字段校验必填注解

应用场景:

  • 一个类中属性a不为空时,属性b不能为空
  • 一个类中属性a不为xxx时,属性b不能为空
  • 一个类中属性a为xxx时,属性b不能为空

注解类

java 复制代码
package com.xxx.common.core.annotation;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

/**
 * @Author zibocoder
 * @Date 2026/04/13
 * @Description 跨字段校验必填注解
 */
@Documented
@Constraint(validatedBy = CrossFieldRequiredValidator.class)
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CrossFieldRequired {
    String message() default "该字段为必填项";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String dependField();

    String expectedValue();

    String targetField();
    
    // 触发方式, 默认非空触发
    TriggerType triggerType() default TriggerType.NOT_EMPTY;

    enum TriggerType {
        NOT_EMPTY,
        NOT_EQUALS,
        EQUALS
    }
}

注解验证器类

java 复制代码
package com.xxx.common.core.annotation;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.lang.reflect.Field;

/**
 * @Author zibocoder
 * @Date 2026/04/13
 * @Description 跨字段校验必填注解验证器
 */
public class CrossFieldRequiredValidator implements ConstraintValidator<CrossFieldRequired, Object> {
    // 被依赖的字段
    private String dependField;
    // 被依赖字段的期望值
    private String expectedValue;
    // 目标字段
    private String targetField;
    // 触发方式
    private CrossFieldRequired.TriggerType triggerType;

    @Override
    public void initialize(CrossFieldRequired constraintAnnotation) {
        this.dependField = constraintAnnotation.dependField();
        this.expectedValue = constraintAnnotation.expectedValue();
        this.targetField = constraintAnnotation.targetField();
        this.triggerType = constraintAnnotation.triggerType();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null) {
            return true;
        }

        try {
            Field dependFld = getField(value.getClass(), dependField);
            if (dependFld == null) {
                return true;
            }
            dependFld.setAccessible(true);
            Object dependValue = dependFld.get(value);

            // 是否应该校验
            boolean shouldValidate = false;
            if (triggerType == CrossFieldRequired.TriggerType.NOT_EMPTY) {
                shouldValidate = dependValue != null && !dependValue.toString().trim().isEmpty();
            } else if (triggerType == CrossFieldRequired.TriggerType.NOT_EQUALS) {
                shouldValidate = dependValue != null && !expectedValue.equals(dependValue.toString());
            } else if (triggerType == CrossFieldRequired.TriggerType.EQUALS) {
                shouldValidate = dependValue != null && expectedValue.equals(dependValue.toString());
            }

            if (shouldValidate) {
                Field targetFld = getField(value.getClass(), targetField);
                if (targetFld == null) {
                    return true;
                }
                targetFld.setAccessible(true);
                Object targetValue = targetFld.get(value);

                if (isEmpty(targetValue)) {
                    context.disableDefaultConstraintViolation();
                    context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                            .addPropertyNode(targetField)
                            .addConstraintViolation();
                    return false;
                }
            }
            return true;
        } catch (Exception e) {
            return true;
        }
    }

    /**
     * 判断对象是否为空
     *
     * @param value 对象
     * @return true:为空 false:不为空
     */
    private boolean isEmpty(Object value) {
        if (value == null) {
            return true;
        }
        if (value instanceof String) {
            return ((String) value).trim().isEmpty();
        }
        return false;
    }

    /**
     * 递归查找字段,支持继承场景
     *
     * @param clazz      类
     * @param fieldName  字段名
     * @return 字段
     */
    private Field getField(Class<?> clazz, String fieldName) {
        while (clazz != null) {
            try {
                return clazz.getDeclaredField(fieldName);
            } catch (NoSuchFieldException e) {
                clazz = clazz.getSuperclass();
            }
        }
        return null;
    }
}

使用示例

java 复制代码
package com.xxx.domain.form;

import com.xxx.common.core.annotation.CrossFieldRequired;
import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
 * @Author zibocoder
 * @Date 2026/04/13
 * @Description 
 */
// 类级别的自定义验证注解,实现跨字段条件校验:当 firstName 字段的值不等于 "现金加油" 时,unitPrice 字段不能为空(null 或空串)。用于确保非现金加油场景下单价为必填项。
@CrossFieldRequired(
    dependField = "firstName", 
    targetField = "lastName", 
    triggerType = CrossFieldRequired.TriggerType.NOT_EMPTY, //默认可不写
    message = "姓氏不为空时,名字也不能为空")
@Data
public class UserAddForm {
    @NotBlank(message = "姓氏不能为空")
    private String firstName;

    private String lastName;
}
相关推荐
她说..12 小时前
MySQL 与 Java 的 JSON 数据处理
java·mysql·json·springboot
Mr.Java.1 天前
本地测试全绿,上线 SQL 报错?Spring Boot 容器级 DT 测试实战指南(附Skills)
java·测试用例·springboot·testcontainers·dt·java测试skills·dt测试
苏渡苇1 天前
Spring Insight 里上报 Span 为什么用 JDK HttpClient 而不是 RestTemplate
java·后端·spring·spring cloud·springboot·性能监控
欢醉2 天前
生产复盘:请求报413 Request Entity Too Large问题分享
springboot·springcloud
慧都小项3 天前
MyEclipse 2026:当 Agent、MCP 与 Java 26 进入 Eclipse 工作流
java·eclipse·springboot·copilot·myeclipse
冬栈程序设计4 天前
Springboot宿舍管理系统【670099】 -附源码(开箱即用)
java·毕业设计·springboot·课程设计·程序设计·大作业
苏生Susheng5 天前
【软件实施】Linux企业运维常用命令手册
java·linux·运维·服务器·springboot·springcloud·软件实施
武哥聊编程6 天前
【AI实战项目】基于LangChain4j+Springboot+Vue+RAG+PGVector的AI智能客服与工单处理系统
springboot·rag·pgvector·langchain4j
小宇宙清歡渡6 天前
Java Stream 流全面实战指南(Spring Boot 3 + JDK 17)
java·springboot·stream流
小宇宙清歡渡7 天前
SpringBoot JDK17 线程池的使用
springboot·线程池·jdk17