在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 extends Payload>[] 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 extends Payload>[] 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方法
}