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

15 阅读2分钟

应用场景:

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

注解类

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

注解验证器类

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

使用示例

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