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

应用场景:

  • 一个类中属性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;
}
相关推荐
luteres35 分钟前
Mybatis+SSM整合笔记
java·开发语言·mybatis
覆东流7 小时前
7.Java数组
java·开发语言·后端
m0_719084117 小时前
限流和获取请求ip的方法
java
threerocks8 小时前
AI 原生软件开发生命周期手册 - 如何借助 AI,逐阶段改造软件开发生命周期
前端·javascript·后端
lzhdim8 小时前
提高 SQL 语句执行速度的方法
java·开发语言·数据库·sql·oracle
JavaPub-rodert8 小时前
王仕宇在 Go Context 如何解决协程泄漏与超时控制
开发语言·后端·golang·iphone·javapub·王仕宇
深漂的华哥8 小时前
Ruoyi-Plus前后端分离场景下,数据加密传输
java·spring boot·后端·开源·maven·ruoyi
落魄大学生之流水线上谋生计10 小时前
Java锁全面指南:从基础概念到企业级应用
java·开发语言
why技术10 小时前
eli5,我觉得这个全网在吹的技能,使用体验真的很一般啊。
前端·人工智能·后端
学习星球10 小时前
Qwik 框架入门实战:从开源项目 Qwik City 开始,用可恢复性替代水合
后端·前端框架·开源·c5全栈