本文面向刚加入 TIS 团队的开发工程师。读完本文,你将理解"伴生操作类"是什么、为什么需要它、以及如何从零开发一个完整的 Manipulate 插件。
一、背景:什么是"伴生操作类"?
TIS 的核心实体(如数据管道 DataXProcessor、本体域 OntologyDomain)本身只负责描述配置,不直接处理用户在界面上触发的"操作"——比如克隆、开启某个功能、触发推断任务等。
这些"操作"由伴生操作类(Manipulate)承担。每个核心实体可以挂载若干个 Manipulate 插件,每个插件对应一种用户可触发的操作。
核心实体(如 OntologyDomain)
└── 伴生操作类 1:EnableChatBI(开启智能问数)
└── 伴生操作类 2:InferOntologyFromLLM(LLM 推断本体关系)
└── 伴生操作类 N:...
用户在界面上点击某个操作按钮时,TIS 框架会找到对应的 Manipulate 实例,调用其 manipuldateProcess 方法。
二、类体系总览
BasicManipuldateProcessor<T> ← 基类,封装持久化通用逻辑(本文重点)
├── DefaultDataXProcessorManipulate ← DataX 管道的伴生操作基类
│ ├── BatchJobCrontab ← 定时任务配置
│ ├── AddMonitorForEvents ← 告警监控配置
│ └── ExportTISPipelineToDolphinscheduler ← 导出到调度系统
└── OntologyDomainManipulate ← 本体域的伴生操作基类
├── EnableChatBI ← 开启智能问数(需持久化)
└── InferOntologyFromLLM ← LLM 推断本体关系(不持久化)
关键包路径:
| 类 | 路径 |
|---|---|
BasicManipuldateProcessor | tis-plugin/.../plugin/manipulate/ |
DefaultDataXProcessorManipulate | tis-plugin/.../datax/ |
OntologyDomainManipulate | tis-plugin/.../plugin/ontology/ |
EnableChatBI / InferOntologyFromLLM | tis-ontology-plugin/.../plugin/ontology/ |
三、核心接口与基类详解
3.1 IPluginStore.ManipuldateProcessor
这是所有 Manipulate 类必须实现的顶层接口,只有一个方法:
interface ManipuldateProcessor {
void manipuldateProcess(IPluginContext pluginContext,
UploadPluginMeta pluginMeta,
Optional<Context> context);
}
你不需要直接实现这个接口。继承 BasicManipuldateProcessor 后,框架已经帮你实现了。
3.2 BasicManipuldateProcessor<T>
这是所有 Manipulate 插件的基类,封装了完整的持久化流程:
用户触发操作
↓
manipuldateProcess() ← final,不可覆盖
↓
ManipuldateUtils.instance() ← 解析请求上下文(是新增/更新/删除?)
↓
loadPluginStore() ← 抽象方法,子类实现,返回对应的 IPluginStore
↓
[删除] deleteFromStore()
[新增/更新] 判重 → replaceInStore()
↓
afterManipuldateProcess() ← 钩子,子类按需覆盖(如触发同步任务)
你需要实现的方法只有两个:
loadPluginStore()(必须实现)
告诉基类"把数据存到哪里"。从 itemsProcessor.getPluginMeta() 中提取上下文(如 domain 名、pipeline 名),返回对应的 IPluginStore。
@Override
protected IPluginStore<MyManipulate> loadPluginStore(
IPluginContext pluginContext,
ManipulateItemsProcessor itemsProcessor) {
// 从 pluginMeta 中取出业务上下文
String domain = itemsProcessor.getPluginMeta()
.getExtraParam(OntologyDomain.NAME_ONTOLOGY_DOMAIN);
KeyedPluginStore.Key<MyManipulate> key =
OntologyDomain.getStoreKey(domain, MyManipulate.class);
return TIS.getPluginStore(key);
}
afterManipuldateProcess()(按需覆盖)
持久化完成后的后续操作。默认是空实现,如果你的操作需要在保存后触发额外逻辑(如同步到 Neo4j、发送通知),在这里写。
@Override
protected void afterManipuldateProcess(
IPluginContext pluginContext,
Optional<Context> context,
ManipulateItemsProcessor itemsProcessor) {
if (itemsProcessor.isDeleteProcess()) {
return; // 删除时通常不需要后续操作
}
// 触发你的业务逻辑
MyService.getInstance().doSomething();
}
3.3 BasicDesc(内部 Descriptor 基类)
每个 Manipulate 插件都需要一个内部静态类 DftDesc(或其他名字),继承对应的 BasicDesc,并加上 @TISExtension 注解。
最关键的方法:isManipulateStorable()
@TISExtension
public static final class DftDesc extends OntologyDomainManipulate.BasicDesc {
@Override
public boolean isManipulateStorable() {
return true; // true = 需要持久化;false = 只执行逻辑,不存储
}
}
isManipulateStorable() 返回值 | 行为 |
|---|---|
true | 基类自动完成判重、持久化(replaceInStore/deleteFromStore),然后调用 afterManipuldateProcess |
false | 跳过持久化,直接调用 afterManipuldateProcess |
四、开发流程:手把手写一个 Manipulate 插件
以"为本体域开启某个新功能"为例,完整步骤如下。
Step 1:确定你的操作属于哪个核心实体
- 操作针对 DataX 数据管道 → 继承
DefaultDataXProcessorManipulate - 操作针对 本体域 → 继承
OntologyDomainManipulate - 操作针对其他实体 → 继承对应的
BasicManipuldateProcessor子类
Step 2:创建操作类
package com.qlangtech.tis.plugin.ontology;
import com.qlangtech.tis.extension.TISExtension;
import com.qlangtech.tis.plugin.annotation.FormField;
import com.qlangtech.tis.plugin.annotation.FormFieldType;
import com.qlangtech.tis.plugin.annotation.Validator;
import com.qlangtech.tis.plugin.ds.manipulate.ManipulateItemsProcessor;
import com.qlangtech.tis.runtime.module.misc.IControlMsgHandler;
import com.qlangtech.tis.util.IPluginContext;
import java.util.Optional;
import com.alibaba.citrus.turbine.Context;
public class MyNewFeature extends OntologyDomainManipulate {
// 在界面上展示的表单字段
@FormField(ordinal = 1, type = FormFieldType.INPUTTEXT,
validate = {Validator.require})
public String someConfig;
@Override
protected void afterManipuldateProcess(
IPluginContext pluginContext,
Optional<Context> context,
ManipulateItemsProcessor itemsProcessor) {
if (itemsProcessor.isDeleteProcess()) {
return;
}
// 持久化完成后,触发你的业务逻辑
String domain = itemsProcessor.getPluginMeta()
.getExtraParam(OntologyDomain.NAME_ONTOLOGY_DOMAIN);
MyService.activate(domain, this.someConfig);
}
@TISExtension
public static final class DftDesc extends OntologyDomainManipulate.BasicDesc {
public DftDesc() {
super();
}
@Override
public boolean isManipulateStorable() {
return true; // 需要持久化
}
@Override
public String getDisplayName() {
return "My New Feature";
}
}
}
Step 3:判断是否需要持久化
问自己一个问题:用户下次打开界面,需要看到这个操作的配置吗?
- 需要(如
EnableChatBI的 LLM 选择)→isManipulateStorable()返回true - 不需要(如
InferOntologyFromLLM只是触发一次推断)→isManipulateStorable()返回false
Step 4:处理删除逻辑
afterManipuldateProcess 在删除操作时也会被调用。通过 itemsProcessor.isDeleteProcess() 判断:
@Override
protected void afterManipuldateProcess(...) {
if (itemsProcessor.isDeleteProcess()) {
// 清理资源,如关闭连接、删除远端配置
MyService.deactivate(domain);
return;
}
// 新增/更新逻辑
MyService.activate(domain, this.someConfig);
}
五、两个典型案例对比
案例 A:EnableChatBI(需持久化 + 有后续操作)
public class EnableChatBI extends OntologyDomainManipulate implements IManipulateStatus {
@FormField(type = FormFieldType.SELECTABLE, ordinal = 1,
validate = {Validator.identity, Validator.require})
public String llm; // 用户选择的大模型
@Override
protected void afterManipuldateProcess(IPluginContext pluginContext,
Optional<Context> context, ManipulateItemsProcessor itemsProcessor) {
if (itemsProcessor.isDeleteProcess()) return;
// 持久化完成后,触发 Neo4j 全量重建
String domainName = itemsProcessor.getPluginMeta()
.getExtraParam(OntologyDomain.NAME_ONTOLOGY_DOMAIN);
OntologySyncQueue.enqueue(
() -> OntologyNeo4jSyncService.getInstance().fullRebuild(domainName));
}
@TISExtension
public static final class DftDesc extends OntologyDomainManipulate.BasicDesc {
@Override
public boolean isManipulateStorable() {
return true; // llm 字段需要持久化,下次打开界面能看到
}
}
}
要点:
isManipulateStorable() = true:llm字段会被保存到 XML 配置文件afterManipuldateProcess里触发异步 Neo4j 同步- 实现了
IManipulateStatus接口,可以在界面上展示当前状态
案例 B:InferOntologyFromLLM(不持久化,只执行一次性任务)
public class InferOntologyFromLLM extends OntologyDomainManipulate {
@FormField(type = FormFieldType.SELECTABLE, ordinal = 1)
public String llm;
@Override
protected void afterManipuldateProcess(IPluginContext pluginContext,
Optional<Context> context, ManipulateItemsProcessor itemsProcessor) {
// 调用 LLM 推断本体关系,结果直接写入本体存储
OntologyPluginMeta ometa = getOntologyPluginMeta(pluginContext, context);
List<OntologyObjectType> objectTypes =
OntologyObjectType.loadAll(ometa.getDomain());
// ... 调用 LLM,解析结果,写入本体 ...
}
@TISExtension
public static final class DftDesc extends OntologyDomainManipulate.BasicDesc {
@Override
public boolean isManipulateStorable() {
return false; // 不需要持久化,每次触发都是全新推断
}
}
}
要点:
isManipulateStorable() = false:基类跳过持久化,直接调用afterManipuldateProcess- 所有业务逻辑都在
afterManipuldateProcess里
六、常见错误与注意事项
❌ 错误 1:覆盖 manipuldateProcess
// 错误!manipuldateProcess 是 final 的,不能覆盖
@Override
public void manipuldateProcess(...) { ... }
正确做法:把逻辑放到 afterManipuldateProcess。
❌ 错误 2:DftDesc 忘记加 @TISExtension
// 错误!没有 @TISExtension,TIS 框架找不到这个 Descriptor
public static final class DftDesc extends OntologyDomainManipulate.BasicDesc { ... }
正确做法:必须加 @TISExtension。
❌ 错误 3:DftDesc 没有继承正确的 BasicDesc
// 错误!继承了错误的基类
public static final class DftDesc extends Descriptor<MyFeature> { ... }
正确做法:必须继承对应的 BasicDesc(OntologyDomainManipulate.BasicDesc 或 DefaultDataXProcessorManipulate.BasicDesc)。
❌ 错误 4:在 afterManipuldateProcess 里忘记处理删除分支
// 危险!删除时也会触发 afterManipuldateProcess
@Override
protected void afterManipuldateProcess(...) {
// 如果是删除操作,这里会报错(domain 已不存在)
MyService.activate(domain);
}
正确做法:先判断 itemsProcessor.isDeleteProcess()。
⚠️ 注意:loadPluginStore 中的上下文提取
itemsProcessor.getPluginMeta().getExtraParam(key) 是从请求的 pluginMeta 中提取参数。不同的核心实体,参数 key 不同:
| 核心实体 | 上下文 key | 获取方式 |
|---|---|---|
| 本体域 | OntologyDomain.NAME_ONTOLOGY_DOMAIN | pluginMeta.getExtraParam(...) |
| DataX 管道 | DataXName | itemsProcessor.getOriginIdentityId() |
七、开发检查清单
开发完成后,对照以下清单自查:
- 操作类继承了正确的基类(
OntologyDomainManipulate或DefaultDataXProcessorManipulate) - 实现了
loadPluginStore()(如果直接继承BasicManipuldateProcessor) - 内部类
DftDesc加了@TISExtension -
DftDesc继承了正确的BasicDesc -
isManipulateStorable()返回值与业务需求一致 -
afterManipuldateProcess里处理了删除分支 - 编译通过:
mvn compile -q
八、总结
| 场景 | 做法 |
|---|---|
| 需要持久化配置 | isManipulateStorable() 返回 true,业务逻辑写在 afterManipuldateProcess |
| 只执行一次性任务 | isManipulateStorable() 返回 false,逻辑全写在 afterManipuldateProcess |
| 持久化后需要触发额外操作 | 覆盖 afterManipuldateProcess,在里面调用业务服务 |
| 克隆/重命名场景 | 覆盖 getNewIdentityName() 返回新名称 |
整个体系的设计思路是模板方法模式:基类 BasicManipuldateProcessor 定义了持久化的完整流程,子类只需填充"存到哪里"(loadPluginStore)和"存完之后做什么"(afterManipuldateProcess)两个空白,其余的校验、判重、存储、删除逻辑由框架统一处理。
有问题随时找团队的同学,欢迎加入 TIS!
