查询过滤与字段填充 SPI 扩展
本页说明查询过滤 SPI 与字段填充 SPI 的使用方式,涵盖三类常见任务:
- 给查询链路自动注入租户、权限、组织等平台级过滤条件。
- 给保存链路自动补齐创建人、更新时间、租户编码等默认字段。
- 在宿主工程中替换平台默认规则,而非直接修改底层框架。
这两组能力的共同目标,是将"平台默认规则"从 geelato-core / geelato-orm 中抽离,改为通过 SPI 在上层工程中显式接入。
先判断这篇适不适合你
如果你的需求是下面这些,优先看本页:
- 想让 MQL 或 Fluent DSL 查询自动带上租户、权限、组织隔离条件
- 想让保存链路统一补齐审计字段、租户字段、组织字段
- 想让不同宿主项目接入自己的平台规则
如果你的需求是下面这些,本页不是首选:
- 只是某一个业务方法里临时拼接一个额外条件
- 只是单条查询临时切数据源
- 只是保存前后想做监听、通知、镜像同步
这类需求优先看:
先判断你要做的是哪一类扩展
在动手前,建议先把场景分清楚:
- 想影响查询:实现“查询过滤 SPI”
- 想影响保存:实现“字段值填充 SPI”
- 想影响 MQL:选 MQL 对应 SPI
- 想影响 Fluent DSL:选 Fluent DSL 对应 SPI
- 想影响对象保存链路:选 Entity Save 对应 SPI
这几个入口相似,但触发位置不同。先选错入口,后面代码写得再完整也不会生效。
架构边界
当前职责边界已经固定:
geelato-core / geelato-orm- 只保留 SPI 契约
- 只保留上下文对象与运行时解析器
- 不承载默认租户规则、默认权限规则、默认字段填充规则
geelato-web-platform- 承载平台默认实现
- 承载默认业务规则
因此如果你的项目要替换默认行为,推荐在业务宿主或平台层实现 SPI,而不是把规则重新写回底层模块。
最短接入步骤
如果你只是想快速接入一条平台规则,可以直接按下面 4 步做。
第 1 步:先决定入口
可以按这张对照表来选:
- MQL 查询:
MqlQueryFilterInjector - Fluent DSL 查询:
FluentQueryFilterInjector - MQL 保存:
MqlSaveFieldValueFiller - Fluent DSL 保存:
FluentSaveFieldValueFiller - 对象保存链路:
EntitySaveFieldValueFiller
一个实用经验是:
- 规则要影响前端/平台通用接口,就优先看 MQL
- 规则要影响后端 Java 服务,就优先看 Fluent DSL
- 规则要影响对象直存链路,就看 Entity Save
第 2 步:实现一个 SPI Bean
推荐直接在宿主工程里新增一个 Spring Bean,而不是先去改底层模块。
第 3 步:保证同类 SPI 只有一个启用实现
同类 SPI 不是“多个一起叠加执行”的设计,而是“最多保留一个启用实现”。
第 4 步:用真实查询或保存链路验证
不要只看 Spring 启动成功。一定要实际触发一条查询或保存,确认你的规则真的进到了执行链路里。
查询过滤 SPI
查询过滤 SPI 用来在查询链路里自动注入平台级条件,例如:
- 租户隔离
- 数据权限
- 组织隔离
- 默认只查有效数据
入口 1:MQL 查询
MQL 查询侧使用:
cn.geelato.core.mql.spi.MqlQueryFilterInjectorcn.geelato.core.mql.spi.support.MqlQueryFilterRuntimeResolver
对应调用链路:
JsonTextQueryParser- 解析出
QueryCommand - 调用
MqlQueryFilterRuntimeResolver.injectIfAvailable(command)
- 解析出
接口形态:
public interface MqlQueryFilterInjector {
boolean isEnabled();
void inject(QueryCommand command);
}
适用场景:
- 页面列表查询
- 平台通用数据接口
- 低代码配置场景
入口 2:Fluent DSL 查询
Fluent DSL 查询侧使用:
cn.geelato.orm.spi.FluentQueryFilterInjectorcn.geelato.orm.spi.support.FluentQueryFilterRuntimeResolver
对应调用链路:
QueryCommandAdapter- 将
MetaQuery适配为QueryCommand - 调用
FluentQueryFilterRuntimeResolver.injectIfAvailable(command, query)
- 将
接口形态:
public interface FluentQueryFilterInjector {
boolean isEnabled();
void inject(QueryCommand command, MetaQuery query);
/**
* 是否强制注入;默认 false。
* 返回 true 时,即使本次查询调用了 MetaQuery.disableInjectFilter() 也会照常注入。
* 实现类通常无需关心,自动继承默认值;仅在需要让某些注入器不可被单次查询绕过时覆盖返回 true。
*/
default boolean isForceInject() {
return false;
}
}
适用场景:
- 后端 Java 服务查询
MetaFactory.query(...)发起的查询链路
平台默认实现
当前平台默认实现位于 geelato-web-platform:
PlatformMqlQueryFilterInjectorPlatformFluentQueryFilterInjectorPlatformQueryFilterSupport
其中 PlatformQueryFilterSupport 负责承载默认租户与权限规则,底层模块不再内置这些规则。
字段值填充 SPI
字段值填充 SPI 用来在保存链路里自动补齐默认字段,例如:
- 创建人
- 创建时间
- 更新人
- 更新时间
- 租户编码
- 组织字段
字段值填充按入口拆成三套 SPI,而不是做成一套大而全的统一接口。
入口 1:MQL Save
cn.geelato.core.mql.spi.MqlSaveFieldValueFillercn.geelato.core.mql.spi.support.MqlSaveFieldValueFillRuntimeResolver
调用入口:
JsonTextSaveParser
接口形态:
public interface MqlSaveFieldValueFiller {
boolean isEnabled();
void fill(MqlSaveFieldValueFillContext context);
}
适用场景:
- 前端通过 MQL 发起保存
- 平台通用保存接口
入口 2:Fluent DSL Save
cn.geelato.orm.spi.FluentSaveFieldValueFillercn.geelato.orm.spi.support.FluentSaveFieldValueFillRuntimeResolver
调用入口:
SaveCommandAdapter
接口形态:
public interface FluentSaveFieldValueFiller {
boolean isEnabled();
void fill(FluentSaveFieldValueFillContext context);
}
适用场景:
- 后端 Java 服务通过
MetaFactory.insert/update(...)发起保存
入口 3:对象保存链路
cn.geelato.core.meta.spi.EntitySaveFieldValueFillercn.geelato.core.meta.spi.support.EntitySaveFieldValueFillRuntimeResolver
调用入口:
EntitySaveParser
接口形态:
public interface EntitySaveFieldValueFiller {
boolean isEnabled();
void fill(EntitySaveFieldValueFillContext context);
}
适用场景:
- 直接走对象保存解析链路的宿主工程
平台默认实现
当前平台默认实现位于 geelato-web-platform:
PlatformMqlSaveFieldValueFillerPlatformFluentSaveFieldValueFillerPlatformEntitySaveFieldValueFillerPlatformFieldValueFillSupport
其中 PlatformFieldValueFillSupport 负责承载默认字段规则,但按入口保留差异,不强行合并成一套全集。
如何实现 SPI
下面给出一套更接近实操的写法。
场景 1:给 MQL 查询补租户过滤
适合场景:
- 前端列表查询统一按租户隔离
- 平台通用接口统一附加权限条件
最小示例:
@Component
public class DemoMqlQueryFilterInjector implements MqlQueryFilterInjector {
@Override
public boolean isEnabled() {
return true;
}
@Override
public void inject(QueryCommand command) {
// 这里根据你的平台规则向 QueryCommand 注入过滤条件
}
}
场景 2:给 Fluent DSL 查询补平台级过滤
适合场景:
- 后端 Java 服务查询自动带租户、权限、组织条件
最小示例:
@Component
public class DemoFluentQueryFilterInjector implements FluentQueryFilterInjector {
@Override
public boolean isEnabled() {
return true;
}
@Override
public void inject(QueryCommand command, MetaQuery query) {
// 这里向 Fluent DSL 查询命令注入平台级过滤条件
}
}
场景 3:给 Fluent DSL 保存补默认字段
适合场景:
- 后端 Java 服务保存时自动补齐创建人、更新时间、租户编码
最小示例:
@Component
public class DemoFluentSaveFieldValueFiller implements FluentSaveFieldValueFiller {
@Override
public boolean isEnabled() {
return true;
}
@Override
public void fill(FluentSaveFieldValueFillContext context) {
// 这里补齐默认保存字段
}
}
场景 4:给 MQL 保存补默认字段
适合场景:
- 前端/平台协议侧保存时统一补字段
最小示例:
@Component
public class DemoMqlSaveFieldValueFiller implements MqlSaveFieldValueFiller {
@Override
public boolean isEnabled() {
return true;
}
@Override
public void fill(MqlSaveFieldValueFillContext context) {
// 这里补齐 MQL 保存链路的默认字段
}
}
统一运行时规则
查询过滤 SPI 与字段值填充 SPI 都遵循同一套运行时规则:
- 扫描到
0个实现:跳过 - 扫描到
1个实现:按isEnabled()决定是否执行 - 扫描到多个实现:直接抛出
IllegalStateException
这套规则一定要记住,因为它直接决定排障方向。
它的意义是:
- 不做隐式回退
- 让实现是否启用保持可见
- 避免多个平台规则并存时出现不透明叠加
这也意味着:
- 不要同时启用两个
MqlQueryFilterInjector - 不要同时启用两个
FluentQueryFilterInjector - 不要同时启用两个
MqlSaveFieldValueFiller - 不要同时启用两个
FluentSaveFieldValueFiller
如果项目里确实存在多个候选实现,必须在宿主层先收敛成唯一启用实现。
单次查询跳过注入(仅 Fluent DSL)
Fluent DSL 查询支持按单次查询跳过注入过滤:在 MetaQuery 上调用 disableInjectFilter() 后,本次查询不会触发任何 FluentQueryFilterInjector,因此平台默认的租户隔离、数据权限等条件都不会附加。
MetaFactory.query("platform_user")
.disableInjectFilter()
.list();
判定规则(在 FluentQueryFilterRuntimeResolver.injectIfAvailable 内):
- 查询未调用
disableInjectFilter():正常注入 - 调用了
disableInjectFilter()且注入器isForceInject()=false:跳过注入 - 调用了
disableInjectFilter()但注入器isForceInject()=true:照常注入(强制覆盖)
isForceInject() 是 FluentQueryFilterInjector 的 default 方法,默认 false,实现类无需关心。仅当某个注入器需要保证"永不被单次查询绕过"(例如租户隔离这类硬边界)时,在其实现中覆盖返回 true。
注意:跳过注入会同时绕过租户隔离与数据权限,存在跨租户、越权读数风险,仅限可信场景(系统后台、定时任务、管理端等)按查询显式使用。
推荐的实操顺序
如果你要在一个新项目里接入 SPI 扩展,推荐顺序是:
- 先确定规则作用在哪条链路上
- 再选对 SPI 接口
- 再实现一个最小 Bean
- 再确认同类 SPI 只有一个启用实现
- 最后用真实查询或保存链路验证
这样做的好处是,一旦出问题,你能快速判断到底是:
- SPI 选错了入口
- Bean 没注册成功
isEnabled()返回了false- 同类实现注册了多个
- 实际业务根本没有走到你以为的那条链路
一步一步排障
如果你感觉“SPI 没生效”,建议按这个顺序排查:
- 先确认当前请求到底走的是 MQL、Fluent DSL,还是对象保存链路
- 再确认你实现的是否是对应入口的 SPI
- 再确认容器里是否只存在一个同类 SPI Bean
- 再确认
isEnabled()是否返回true - 再通过真实查询或保存链路触发一次
- 如果报出
Multiple ... beans found,优先检查是否同时注册了多个实现 - 如果查询"少了"预期的租户/权限过滤,确认调用方是否对本次查询调用了
MetaQuery.disableInjectFilter(),或注入器是否被isEnabled()关闭
对字段值填充而言,还需要特别注意:
SaveDefaultValueFiller / DefaultSaveDefaultValueFiller现在只是兼容层- 当前推荐主扩展入口是:
MqlSaveFieldValueFillerFluentSaveFieldValueFillerEntitySaveFieldValueFiller
BaseEntityMetaObjectHandler当前仍保持现状,不等于当前这套 SPI 主链路
扩展时的注意事项
- 不要把平台规则重新写回
geelato-core / geelato-orm - 查询过滤与字段填充应尽量做成“框架级复用规则”,不要退化成业务散点补丁
- 如果规则只适用于某一个入口,就实现对应入口的 SPI,不要强行做一套统一总接口
- 如果只是个别接口的局部逻辑,优先放在业务层,不要滥用 SPI
与旧能力的关系
SaveDefaultValueFiller / DefaultSaveDefaultValueFiller 当前仍保留在 geelato-orm 中,但它们已经降级为兼容层,不再是 SaveCommandAdapter 主链路的推荐扩展入口。
如果你要扩展当前保存链路,优先使用:
MqlSaveFieldValueFillerFluentSaveFieldValueFillerEntitySaveFieldValueFiller