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. 引入依赖
- 2. 继承 ValidateSupport
- 3. Fluent 模式(链式 API)
- 4. Yavi 模式(声明式 API)
- 5. 13 个 Validator 参考
- 6. 错误处理
- 7. 自动配置
- 8. 常见问题
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 怎么选
| 维度 | Fluent | Yavi |
|---|---|---|
| API 风格 | 命令式链式(运行时拼装) | 声明式构建器(预编译) |
| 注解支持 | ✅ 内置 JSR303 | ❌ 纯编程式 |
| 规则定义时机 | 每次 validate 都 new ValidatorDelegate | 启动时构建一次 Validator 复用 |
| 异常 | FluentValidateException(@Deprecated) | ValidateException(推荐) |
| 适用场景 | 条件复杂、规则动态、需要 JSR303 注解 | 规则固定、性能敏感 |
建议:两者可以共存。同一个 Service 里不同方法用不同引擎。
5. 13 个 Validator 参考
全部 extends ValidatorHandler<T>,有两种构造模式:
| Validator | 泛型 | 检查逻辑 | Fluent byError |
|---|---|---|---|
| NotNullValidator | Object | null 检查 | ✅ |
| NotBlankValidator | String | 空字符串/纯空白(hutool StrUtil.isBlank) | ✅ |
| NotEmptyValidator | Collection | 空集合 | ✅ |
| LengthValidator | String | 长度 | ✅ |
| LengthIgnoreNoneValidator | String | 长度(忽略 None 值) | ✅ |
| RangeValidator | Number | min/max 范围 | ✅ |
| RegexValidator | String | 正则匹配 | ✅ |
| EqualToValidator | Object | 等于指定值 | ✅ |
| NotEqualValidator | Object | 不等于指定值 | ✅ |
| ExistValidator | Object | 值在指定 Collection 中 | ✅ |
| NotExistValidator | Object | 值不在指定 Collection 中 | ✅ |
| ExistKeyValidator | Object | key 在 Map 中存在 | ✅ |
| NotExistKeyValidator | Object | key 在 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。