You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Boot中如何优雅验证依赖其他属性的字段?

解决依赖属性的Bean校验问题(含无效值与属性名优化)

首先,针对你当前遇到的getInvalidValue()返回整个Bean而非status值的问题,我们可以通过Hibernate Validator的API调整,同时优化属性名的硬编码问题;另外也会介绍更简洁的替代方案。

一、修复自定义Validator的无效值与硬编码问题

1. 设置正确的无效值

在你的StatusValidator中,ConstraintViolationBuilder.NodeBuilderCustomizableContext提供了invalidateValue(Object)方法,可以指定当前属性节点的无效值为status的实际值,而不是整个Foo对象。

2. 避免硬编码属性名

可以通过方法引用+反射的方式从getter方法中自动提取属性名,无需手动维护字符串常量。

修改后的StatusValidator代码如下:

import java.beans.Introspector;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.function.Function;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class StatusValidator implements ConstraintValidator<StatusValidate, Foo> {
    private String statusPropertyName;

    @Override
    public void initialize(StatusValidate constraintAnnotation) {
        // 通过Foo::getStatus方法引用自动提取属性名
        this.statusPropertyName = extractPropertyName(Foo::getStatus);
    }

    @Override
    public boolean isValid(Foo value, ConstraintValidatorContext context) {
        if (value == null) {
            return true; // 空对象校验交给@NotNull等注解
        }

        LocalDate completionDate = value.getCompletionDate();
        String status = value.getStatus();

        if (completionDate != null && !Arrays.asList("complete", "closed").contains(status.toLowerCase())) {
            context.disableDefaultConstraintViolation();
            context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
                    .addPropertyNode(statusPropertyName)
                    .invalidateValue(status) // 设置无效值为status的实际内容
                    .addConstraintViolation();
            return false;
        }
        return true;
    }

    // 工具方法:从getter方法引用中提取属性名
    private <T> String extractPropertyName(Function<T, ?> getter) {
        try {
            Method method = ((MethodHandles.Lookup) getter.getClass().getMethod("lookup").invoke(getter))
                    .findVirtual(getter.getClass(), "invoke", MethodType.methodType(Object.class))
                    .getMethod();
            String methodName = method.getName();
            
            if (methodName.startsWith("get")) {
                return Introspector.decapitalize(methodName.substring(3));
            } else if (methodName.startsWith("is")) {
                return Introspector.decapitalize(methodName.substring(2));
            }
            throw new IllegalArgumentException("Provided function is not a valid getter method");
        } catch (Exception e) {
            throw new RuntimeException("Failed to extract property name from getter method", e);
        }
    }
}

这样修改后,constraintViolation.getInvalidValue()会返回status的实际值,且属性名无需硬编码。

二、更简洁的依赖属性校验方案(无需自定义Validator)

如果不想编写自定义Validator,可以使用Hibernate Validator提供的@ScriptAssert注解,它支持类级的跨属性校验,且能直接指定错误报告的属性。

1. 使用@ScriptAssert实现校验

在Foo类上添加@ScriptAssert注解,通过EL表达式编写校验逻辑,并指定reportOn参数将错误关联到status属性:

import javax.validation.constraints.ScriptAssert;

@ScriptAssert(
        lang = "el",
        script = "_this.completionDate == null or (_this.status.toLowerCase() == 'complete' or _this.status.toLowerCase() == 'closed')",
        message = "status must be 'complete' or 'closed' when completionDate is not null",
        reportOn = "status"
)
public class Foo {
    private String status;
    private LocalDate completionDate;

    // getters and setters
}

此时,校验错误的getPropertyPath()会返回statusgetInvalidValue()会返回status的实际值,完全满足你的需求。

2. 优化@ScriptAssert的硬编码问题

为了避免reportOn和EL表达式中的属性名硬编码,可以在Foo类中定义常量:

import javax.validation.constraints.ScriptAssert;

public class Foo {
    public static final String STATUS_FIELD = "status";
    public static final String COMPLETION_DATE_FIELD = "completionDate";

    private String status;
    private LocalDate completionDate;

    @ScriptAssert(
            lang = "el",
            script = "_this." + COMPLETION_DATE_FIELD + " == null or (_this." + STATUS_FIELD + ".toLowerCase() == 'complete' or _this." + STATUS_FIELD + ".toLowerCase() == 'closed')",
            message = "status must be 'complete' or 'closed' when completionDate is not null",
            reportOn = STATUS_FIELD
    )
    // getters and setters
}

这样只需维护一次属性名字符串,减少后续维护成本。

三、其他注意事项

  • 若使用@Validated分组校验,确保自定义注解或@ScriptAssertgroups属性包含你的分组类(如Update.class)。
  • 空值处理:建议单独使用@NotNull等注解处理单个字段的空值校验,避免在跨属性校验中混杂空值判断逻辑。

内容的提问来源于stack exchange,提问作者Smile

火山引擎 最新活动