应用场景:
- 一个类中属性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;
}