Spring Boot验证新境界!四类自定义验证器实战揭秘

本文详细介绍了在Spring Boot中构建有状态自定义Bean验证器的完整方案,涵盖单字段验证、多字段交叉验证、基于配置的验证和数据库驱动验证四种典型场景。通过实际代码示例展示了如何创建自定义验证注解、实现验证逻辑、处理复杂业务规则验证,为开发企业级应用提供了实用的验证解决方案。

在Spring Boot应用开发中,Bean Validation通过Hibernate Validator提供了开箱即用的验证支持。我们可以使用标准注解如@NotNull来验证请求对象的字段,但在实际业务场景中,往往需要扩展验证功能,甚至需要基于运行时数据实现验证逻辑。本文将详细介绍如何构建自定义验证器,包括单字段验证、多字段交叉验证以及基于Spring配置和数据库的状态验证。

单字段自定义验证器实现

首先我们实现一个产品ID校验位验证器。产品ID格式要求为"A-八位数字-一位校验码",其中校验码是前八位数字之和的末位。

创建验证注解:

java
@Constraint(validatedBy = ProductCheckDigitValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ProductCheckDigit {
    String message() default "必须具有有效校验位";
    Class[] groups() default {};
    Class[] payload() default {};
}

实现验证器逻辑:

java
public class ProductCheckDigitValidator implements ConstraintValidator {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        if (value == null) {
            return false;
        }
        String[] parts = value.split("-");
        return parts.length == 3 && checkDigitMatches(parts[1], parts[2]);
    }

    private boolean checkDigitMatches(String productCode, String checkDigit) {
        int sumOfDigits = IntStream.range(0, productCode.length())
                .map(character -> Character.getNumericValue(productCode.charAt(character)))
                .sum();
        int checkDigitProvided = Character.getNumericValue(checkDigit.charAt(0));
        return checkDigitProvided == sumOfDigits % 10;
    }
}

在实体类中使用注解:

java
public class PurchaseOrderItem {
    @ProductCheckDigit
    @NotNull
    @Pattern(regexp = "A-\\d{8}-\\d")
    private String productId;
    // 其他字段...
}

测试用例显示,当产品ID为A-12345678-6时验证通过,因为1+2+3+4+5+6+7+8=36,末位为6。而设置为A-12345678-1时验证失败,返回相应错误信息。

多字段交叉验证器

接下来实现采购订单项的数量选择验证,要求要么选择个体数量,要么选择包数量,不能同时选择或同时不选。

创建类级别注解:

java
@Constraint(validatedBy = ChoosePacksOrIndividualsValidator.class)
@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface ChoosePacksOrIndividuals {
    String message() default "";
    Class[] groups() default {};
    Class[] payload() default {};
}

实现多字段验证逻辑:

java
public class ChoosePacksOrIndividualsValidator implements ConstraintValidator {
    
    @Override
    public boolean isValid(PurchaseOrderItem value, ConstraintValidatorContext context) {
        context.disableDefaultConstraintViolation();
        boolean isValid = true;
        
        if ((value.getNumberOfPacks() == 0) == (value.getNumberOfIndividuals() == 0)) {
            isValid = false;
            if (value.getNumberOfPacks() == 0) {
                context.buildConstraintViolationWithTemplate("当没有包数量时必须选择个体数量")
                        .addPropertyNode("numberOfIndividuals")
                        .addConstraintViolation();
                context.buildConstraintViolationWithTemplate("当没有个体数量时必须选择包数量")
                        .addPropertyNode("numberOfPacks")
                        .addConstraintViolation();
            } else {
                context.buildConstraintViolationWithTemplate("不能与包数量同时使用")
                        .addPropertyNode("numberOfIndividuals")
                        .addConstraintViolation();
                context.buildConstraintViolationWithTemplate("不能与个体数量同时使用")
                        .addPropertyNode("numberOfPacks")
                        .addConstraintViolation();
            }
        }
        
        if (value.getNumberOfPacks() > 0 && value.getItemsPerPack() == 0) {
            isValid = false;
            context.buildConstraintViolationWithTemplate("使用包数量时不能为0")
                    .addPropertyNode("itemsPerPack")
                    .addConstraintViolation();
        }
        
        return isValid;
    }
}

基于Spring配置的状态验证

现在实现基于运行时配置的租户渠道验证,从application.properties加载可用渠道列表。

配置属性类:

java
@ConfigurationProperties("com.baeldung.tenant")
public class TenantChannels {
    private String[] channels;
    // getter和setter方法
}

实现配置驱动的验证器:

java
public class AvailableChannelValidator implements ConstraintValidator {
    
    @Autowired
    private TenantChannels tenantChannels;
    private Set channels;
    
    @Override
    public void initialize(AvailableChannel constraintAnnotation) {
        channels = Arrays.stream(tenantChannels.getChannels()).collect(Collectors.toSet());
    }
    
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return channels.contains(value);
    }
}

基于数据库的状态验证

最后实现基于数据库仓库路由信息的验证,确保源仓库可以发货到目的国家。

仓库路由仓库类:

java
@Repository
public class WarehouseRouteRepository {
    public boolean isWarehouseRouteAvailable(String sourceWarehouse, String destinationCountry) {
        // 数据库查询逻辑
        return true; // 简化实现
    }
}

数据库驱动的验证器:

java
public class AvailableWarehouseRouteValidator implements ConstraintValidator {
    
    @Autowired
    private WarehouseRouteRepository warehouseRouteRepository;
    
    @Override
    public boolean isValid(PurchaseOrderItem value, ConstraintValidatorContext context) {
        return warehouseRouteRepository.isWarehouseRouteAvailable(
            value.getSourceWarehouse(), 
            value.getDestinationCountry()
        );
    }
}

完整实体类示例

java
@ChoosePacksOrIndividuals
@AvailableWarehouseRoute
public class PurchaseOrderItem {
    @ProductCheckDigit
    @NotNull
    @Pattern(regexp = "A-\\d{8}-\\d")
    private String productId;
    
    private String sourceWarehouse;
    private String destinationCountry;
    
    @AvailableChannel
    private String tenantChannel;
    
    private int numberOfIndividuals;
    private int numberOfPacks;
    private int itemsPerPack;
    
    @org.hibernate.validator.constraints.UUID
    private String clientUuid;
    
    // getter和setter方法
}