一佳技术

easy-validate 使用文档

easy-validate 把两个独立的校验引擎(FluentValidator 链式 API + Yavi 声明式 API)包在统一门面基类 ValidateSupport 下。业务 Service / DomainService 继承 ValidateSupport,用 validate(entity, Consumer<ValidatorDelegate>) 走 Fluent 链式,用 validate(entity, yaviValidator) 走 Yavi 声明式。两种模式共享同一套错误模型(ValidateError / ValidateException)。提供 13 个 Validator 实现覆盖常见业务场景。


1. 引入依赖

<!-- 核心依赖(必需) -->
<dependency>
    <groupId>io.matrix</groupId>
    <artifactId>easy-validate</artifactId>
    <version>${revision}</version>
</dependency>

<!-- 生产环境需要 JSR303 实现(FluentValidator 模式用) -->
<!-- spring-boot-starter-validation 会带 HibernateValidator -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

FluentValidator 和 Yavi 都是 compile scope 硬依赖,不需要额外引入。但 HibernateValidator 在 easy-validate pom.xml 里是 test scope——应用必须自己提供。


2. 继承 ValidateSupport

ValidateSupport 是 abstract 基类,业务 Service / DomainService 继承它即可获得两套校验 API:

@Service
public class ProductService extends ValidateSupport {

    // 使用方的业务方法...
    public void createProduct(Product product) {
        // 先用 Fluent 校验
        validate(product, delegate -> {
            delegate.notBlank(product.getName(), "商品名称必填")
                    .range(product.getPrice(), "价格必须大于 0")
                    .notNull(product.getCategory(), "分类必填");
        });

        // 业务逻辑...
        productRepository.save(product);
    }
}

ValidateSupport 不是 Spring Bean,它的字段通过子类所在的 Spring 容器自动注入(@Autowired Registry + @Resource(name="fluentHibernateValidator"))。


3. Fluent 模式(链式 API)

3.1 基本用法

public void createProduct(Product product) {
    validate(product, delegate -> {
        // 13 个 fluent 方法,全部返回 ValidatorDelegate 支持链式
        delegate.notBlank(product.getName(), "商品名称不能为空")
                .length(product.getName(), "名称长度 2-50 字符")
                .range(product.getPrice(), "价格必须在 0.01-100000 之间")
                .regex(product.getSku(), "SKU 格式不正确");
    });
}
// 校验失败自动抛 FluentValidateException(@Deprecated,内含 ValidateError[])

3.2 JSR303 注解校验

Fluent 模式内置了 HibernateValidator JSR303 注解校验——链式 Validator 执行之前会先跑一遍 entity 上的 @NotNull / @Size / @Min / @Email 等注解:

public class Product {
    @NotBlank(message = "名称不能为空")
    @Size(min = 2, max = 50)
    private String name;

    @NotNull
    @DecimalMin(value = "0.01")
    private BigDecimal price;

    // Lombok @Data ...
}

@Service
public class ProductService extends ValidateSupport {
    public void createProduct(Product product) {
        validate(product, delegate -> {
            // 先跑 JSR303 注解(@NotBlank/@Size/@NotNull/@DecimalMin)
            // 再跑链式自定义校验
            delegate.notExist(product.getSku(), "SKU 已存在");
        });
    }
}

3.3 链式方法参考

delegate
    // 空值检查
    .notNull(value, "不能为空")
    .notBlank(str, "不能为空字符串")
    .notEmpty(collection, "不能为空集合")

    // 长度
    .length(str, "长度超出范围")           // LengthValidator.byError(msg)
    .lengthIgnoreNone(str, "长度超出范围")  // LengthIgnoreNoneValidator

    // 范围
    .range(number, "超出范围")             // RangeValidator.byError(msg)

    // 正则
    .regex(str, "格式不正确")

    // 相等/不等
    .equalTo(value, "必须等于")
    .notEqual(value, "不能等于")

    // 存在性(值在集合/Map 中)
    .exist(value, "不存在")        // ExistValidator
    .notExist(value, "已存在")      // NotExistValidator
    .existKey(key, "key 不存在")   // ExistKeyValidator
    .notExistKey(key, "key 已存在") // NotExistKeyValidator

4. Yavi 模式(声明式 API)

Yavi 是纯 Java 声明式校验框架(am.ik.yavi:yavi:0.16.0),提前构建不可变 Validator 对象,执行时只传入实体。优点:Validator 可预编译复用、性能好、规则清晰。

4.1 构建 Validator

import am.ik.yavi.builder.ValidatorBuilder;
import am.ik.yavi.core.Validator;

@Service
public class ProductService extends ValidateSupport {

    // 启动时构建一次,全局复用
    private final Validator<Product> productValidator = ValidatorBuilder.of(Product.class)
        .constraint(Product::getName, "name", c -> c.notBlank().message("名称不能为空"))
        .constraint(Product::getPrice, "price", c -> c.positive().message("价格必须为正"))
        .constraint(Product::getSku, "sku", c -> c.regex("^SKU-\\d+$").message("SKU 格式不正确"))
        .build();

    public void createProduct(Product product) {
        // Yavi 校验失败直接抛 ValidateException
        validate(product, productValidator);

        // 业务逻辑...
        productRepository.save(product);
    }
}

4.2 只校验不抛

ConstraintViolations violations = validateOnly(product, productValidator);
if (violations.isNotEmpty()) {
    List<String> messages = extractMessages(violations);   // ["名称不能为空", "价格必须为正"]
    List<String> fields = extractFieldNames(violations);   // ["name", "price"]
    throw new ValidateException(messages);
}

4.3 Fluent vs Yavi 怎么选

维度FluentYavi
API 风格命令式链式(运行时拼装)声明式构建器(预编译)
注解支持✅ 内置 JSR303❌ 纯编程式
规则定义时机每次 validate 都 new ValidatorDelegate启动时构建一次 Validator 复用
异常FluentValidateException(@Deprecated)ValidateException(推荐)
适用场景条件复杂、规则动态、需要 JSR303 注解规则固定、性能敏感

建议:两者可以共存。同一个 Service 里不同方法用不同引擎。


5. 13 个 Validator 参考

全部 extends ValidatorHandler<T>,有两种构造模式:

Validator泛型检查逻辑Fluent byError
NotNullValidatorObjectnull 检查✅
NotBlankValidatorString空字符串/纯空白(hutool StrUtil.isBlank)✅
NotEmptyValidatorCollection空集合✅
LengthValidatorString长度✅
LengthIgnoreNoneValidatorString长度(忽略 None 值)✅
RangeValidatorNumbermin/max 范围✅
RegexValidatorString正则匹配✅
EqualToValidatorObject等于指定值✅
NotEqualValidatorObject不等于指定值✅
ExistValidatorObject值在指定 Collection 中✅
NotExistValidatorObject值不在指定 Collection 中✅
ExistKeyValidatorObjectkey 在 Map 中存在✅
NotExistKeyValidatorObjectkey 在 Map 中不存在✅

注解式路径(new XxxValidator(fieldName))需要 Validator 加 @Component 才能被 SpringRegistry 发现。当前 13 个 Validator 都没有 @Component,所以注解式路径实际没走通——Fluent 路径(手动 new)是主力。


6. 错误处理

ValidateException(推荐)

// 抛出统一异常
throw new ValidateException(List.of(error1, error2));
throw new ValidateException("name", "名称不能为空");

// 从异常提取错误
exception.getErrors()     // List<ValidateError>(不可变副本)
exception.getMessages()   // ["名称不能为空", "价格必须为正"]
exception.getFields()     // ["name", "price"]
exception.buildMessage()  // "name: 名称不能为空; price: 价格必须为正"

FluentValidateException(@Deprecated)

Fluent 模式仍会抛它(因为 ValidateSupport 的 Fluent 路径内部 catch 了 ValidationError 转换),但它已标记废弃。建议逐步迁移到统一的 ValidateException。

ValidateError

public class ValidateError {
    private final String field;    // 字段名
    private final String message;  // 错误消息
}

两个引擎都会把各自的错误类型(Fluent ValidationError / Yavi ConstraintViolation)转换成 ValidateError。


7. 自动配置

spring.factories 注册两个配置类平级:

配置类条件注册 Bean
FluentValidateConfiguration无条件fluentHibernateValidator(javax.validation.Validator)+ SpringApplicationContextRegistry
YaviValidateAutoConfiguration@ConditionalOnClass(ValidatorBuilder.class)空壳(无 Bean 产出)

运行时依赖:fluentHibernateValidator Bean 创建需要 JSR303 实现。easy-validate pom.xml 把 hibernate-validator 放在 test scope——应用必须自行提供(spring-boot-starter-validation 会带)。


8. 常见问题

Q: ValidateSupport 为什么是 abstract? A: 它给业务代码当基类用。@Autowired Registry + @Resource fluentHibernateValidator 字段靠子类所在的 Spring 容器自动注入。

Q: Fluent 和 Yavi 能混用吗? A: 完全可以。同一个 Service 里不同方法用不同重载。Fluent 适合条件复杂、需要 JSR303 注解校验的场景;Yavi 适合规则固定、性能敏感的场景。

Q: 13 个 Validator 没有 @Component,注解式路径能用吗? A: 不能。SpringRegistry 自动发现需要 Validator 是 Spring Bean,但 13 个 Validator 都没加 @Component。Fluent 路径是手动 new(XxxValidator.byError(msg)),不依赖 Spring Registry。

Q: RangeValidator.byError() 为什么没 min/max 参数? A: 设计如此——Fluent 路径传自定义 message,范围检查逻辑内部有默认值(min=0, max=Long.MAX_VALUE)。要指定真实范围得走注解式路径 new RangeValidator(fieldName, min, max)。

Q: NotBlankValidator 为什么用 hutool StrUtil? A: StrUtil.isBlank() 比 JDK String.isBlank() 多了空白字符的处理(如全角空格、制表符等),更贴近中文场景。hutool 已经是 matrix-framework 通用依赖。

Q: 新增自定义 Validator 怎么做? A: 继承 ValidatorHandler<T> 实现 Validator<T> 接口,写 validate() 方法返回 true/false,提供 (fieldName) 构造器和 static XxxValidator byError(msg) 工厂。然后在 ValidatorDelegate 里加一个 fluent 方法调用它。

Q: FluentValidateException 和 ValidateException 有什么区别? A: FluentValidateException 是 @Deprecated,持有 Fluent 原生 ValidationError + 转成 ValidateError 传给父类。ValidateException 只持有 ValidateError。业务代码应该只 catch ValidateException。