MITK中两种微服务的三层架构实现对比
两种微服务的三层架构实现对比
按 OSGi 规范的三层架构(模块层 / 生命周期层 / 服务层)对比
CppMicroServices(us::)与 CTK PluginFramework 的实现细节。源码位置:
- CppMicroServices:
D:/MITK/Modules/CppMicroServices/core/src/- CTK:
D:/MITK-superbuild-2023.12/ep/src/CTK/Libs/PluginFramework/
OSGi 规范定义三层:模块层(Module Layer,打包与依赖)、
生命周期层(Lifecycle Layer,安装/启动/停止)、
服务层(Service Layer,注册/发现)。两套实现对三层的完成度截然不同。
一、模块层(Module Layer)——“模块是什么、依赖怎么声明”
| CppMicroServices(us::) | CTK | |
|---|---|---|
| 模块单位 | 普通动态库(.dll/.so),无特殊打包 | CTK 插件(带 META-INF/MANIFEST.MF 的 Qt 插件) |
| 元数据 | 可选manifest.json(版本、autoLoadDir) | MANIFEST.MF 强制:Plugin-SymbolicName、Plugin-Version、Require-Plugin、Plugin-ActivationPolicy(ctkPluginConstants.h: L132-240) |
| 依赖声明 | 没有——模块间依赖靠链接器(编译期)解决 | Require-Plugin: com.acme.test; plugin-version="[1.0,2.0)"——带版本区间的运行期依赖(L204-209) |
| 依赖解析 | 无解析过程 | checkRequirePlugin:递归解析 Require 链 + 版本区间匹配 |
| 资源系统 | 编译进二进制的资源容器(usModuleResourceContainer) | 资源存SQLite(PLUGIN_RESOURCES_TABLE) |
| 身份注册 | ModuleRegistry: name → Module*(自增 id) | ctkPlugins: location(URL) → QSharedPointer<ctkPlugin>(SQLite 持久化安装状态) |
CTK 依赖解析实现(ctkPluginFrameworkContext.cpp: L269-300)
voidctkPluginFrameworkContext::checkRequirePlugin(ctkPluginPrivate*plugin){QListIterator<ctkRequirePlugin*>i(plugin->require);// MANIFEST 里的 Require-Plugin 列表while(i.hasNext()){ctkRequirePlugin*pr=i.next();// 按名字 + 版本区间找候选插件QList<ctkPlugin*>pl=plugins->getPlugins(pr->name,pr->pluginRange);for(...每个候选 p2...){if(tempResolved.contains(p2))ok=p2;// 正在本轮解析中(处理循环依赖)elseif(RESOLVED_FLAGS&p2->state)ok=p2;// 已解析elseif(p2->state==ctkPlugin::INSTALLED)// 递归解析依赖的依赖(tempResolved 集合防环)...}}}核心差异
us:: 的模块层是退化的——没有版本、没有依赖解析、没有安装概念,
模块 = 已被 OS 加载的库。CTK 实现了完整 OSGi 模块层——
版本区间依赖、解析失败可诊断、安装状态跨进程持久(SQLite)。
二、生命周期层(Lifecycle Layer)——“状态机与钩子”
CTK:完整六态状态机(ctkPlugin.h: L87-103)
UNINSTALLED ← INSTALLED → RESOLVED → STARTING → ACTIVE ↑ │ └──── STOPPING ←─────┘- INSTALLED→RESOLVED:
getUpdatedState(ctkPlugin_p.cpp: L174-198)
触发依赖解析,成功才置 RESOLVED 并广播事件——"装了"和"能用"是两个状态 - 启动:
start0()(ctkPlugin_p.cpp: L720-790):
ctkPluginException*ctkPluginPrivate::start0(){fwCtx->listeners.emitPluginChanged(ctkPluginEvent(STARTING,...));pluginLoader.load();// QPluginLoader 加载库pluginActivator=qobject_cast<ctkPluginActivator*>(pluginLoader.instance());// Qt 插件根对象即 ActivatorpluginActivator->start(pluginContext.data());// 回调 start()state=ctkPlugin::ACTIVE;...// 异常回滚路径(L780-789):// STOPPING → removePluginResources() → context invalidate → 退回 RESOLVED}- 每一步状态迁移都发 ctkPluginEvent(STARTING/STARTED/STOPPING/STOPPED)
- 支持懒激活(
Plugin-ActivationPolicy: eager/lazy) - start 中途被 uninstall 有专门检测(STATECHANGE_ERROR)
Activator 接口(ctkPluginActivator.h: L71-111):
classctkPluginActivator{virtualvoidstart(ctkPluginContext*context)=0;virtualvoidstop(ctkPluginContext*context)=0;};CppMicroServices:二态(usModule.h: L64,166)
未加载 ⇄ 已加载(IsLoaded() 一个 bool)- 加载即注册即启动:静态初始化(
US_INITIALIZE_MODULE)→ModuleRegistry::Register→Module::Start一气呵成,没有中间可停留的状态 - Activator 获取用原生符号查找(
usModule.cpp: L133-135):
std::string activator_func="_us_module_activator_instance_"+d->info.name;void*sym=ModuleUtils::GetSymbol(d->info,activator_func.c_str());// dlsym 等价d->moduleActivator=activatorHook();d->moduleActivator->Load(d->moduleContext);- 事件只有 LOADING/LOADED/UNLOADING/UNLOADED 四种,无 RESOLVED 概念
- 无异常回滚状态机:
Activator::Load抛异常直接向上传播
(L152-156 注释明说"语义上视为 noexcept")
核心差异
CTK 的生命周期与 .so 加载解耦——库可以加载着但插件停在 RESOLVED 未激活;
BlueBerry ExtensionRegistry 正是利用这一点:插件 RESOLVED 就解析它的 plugin.xml,
不必 start(懒激活的基础)。us:: 的生命周期与 .so 加载同一,
简单但无法表达"就绪未启动"。
三、服务层(Service Layer)——“几乎是同一份代码”
这一层两者都是 Knopflerfish 的完整移植,结构逐字段对应:
| 机制 | CppMicroServices | CTK | 差异 |
|---|---|---|---|
| 双表存储 | services+classServices(usServiceRegistry_p.h: L63-80) | 同名双表(ctkServices_p.h: L64-71) | 仅容器(STL vs QHash) |
| ranking 排序 | lower_bound有序插入,Get取back() | 同 | 无 |
| 服务本体 | InterfaceMap = std::map<string, void*> | QObject*+QStringList接口名 | CTK 服务必须是 QObject,用qobject_cast校验接口 |
| 使用记账 | dependents: Map<Module*,int> | dependents: QHash<QSharedPointer<ctkPlugin>,int> | 同构 |
| 作用域 | singleton / module / prototype 三种(SERVICE_SCOPE) | singleton + ctkServiceFactory | us:: 多 prototype |
| 事件分发 | 回调接口 + 监听器分桶(objectclass/service.id 两级 cache) | Qt 信号槽(ctkServiceSlotEntry:receiver+slot 名) | 事件投递机制不同 |
| LDAP 过滤 | usLDAPExpr | ctkLDAPExpr | 同一算法 |
| 自动清理 | RemoveModuleResources:卸载时注销全部服务+归还引用 | removePluginResources():stop 时同样操作 | 同构 |
| 跟踪器 | us::ServiceTracker(.tpp 模板) | ctkServiceTracker | 同构 |
核心差异只有两点
- CTK 服务绑定 Qt 元对象系统:服务必须是 QObject、事件走信号槽、
接口用Q_DECLARE_INTERFACE+qobject_cast;us:: 零 Qt 依赖靠模板。 - 锁粒度:CTK 部分用 QReadWriteLock,us:: 单 Mutex。
总结:一张表看三层完成度
| OSGi 层 | CppMicroServices | CTK |
|---|---|---|
| 模块层 | ◐ 退化:无版本/无依赖解析,模块=已加载的库 | ● 完整:MANIFEST + Require-Plugin 版本区间 + 递归解析 + SQLite 持久化 |
| 生命周期层 | ◐ 二态(加载/未加载),静态初始化自动走完 | ● 完整六态状态机 + 事件 + 懒激活 + 异常回滚 |
| 服务层 | ● 完整(Knopflerfish 移植) | ● 完整(同一份 Knopflerfish 移植,Qt 化) |
这解释了 MITK 的分工为什么合理:
- Modules 层用 us::——算法库不需要"安装/解析/延迟启动"这些重量级语义,
加载即用、零依赖最合适 - Plugins 层用 CTK——UI 插件需要版本依赖、按需激活、
RESOLVED 阶段读 plugin.xml,非完整生命周期不可
两者服务层同源同构,所以开发者跨层切换时 API 心智模型几乎不变。
关键源码文件索引
CppMicroServices(Modules/CppMicroServices/core/下)
| 层 | 内容 | 文件 | 关键行 |
|---|---|---|---|
| 模块层 | 模块表 | src/module/usModuleRegistry.cpp | L39-73 |
| 模块层 | manifest.json 解析 | src/module/usModulePrivate.cpp | L53-68 |
| 生命周期层 | 二态判定 | include/usModule.h | L64, L166(IsLoaded) |
| 生命周期层 | Start/符号查找 | src/module/usModule.cpp | L119-171 |
| 生命周期层 | 自动注册宏 | include/usModuleInitialization.h | L57-120 |
| 服务层 | 服务双表 | src/service/usServiceRegistry_p.h | L63-80 |
| 服务层 | 注册私有体/dependents | src/service/usServiceRegistrationBasePrivate.h | L40-115 |
| 服务层 | 监听器分桶 | src/service/usServiceListeners_p.h | L43-65 |
CTK(ep/src/CTK/Libs/PluginFramework/下)
| 层 | 内容 | 文件 | 关键行 |
|---|---|---|---|
| 模块层 | MANIFEST 头常量 | ctkPluginConstants.h | L132-240 |
| 模块层 | Require-Plugin 解析 | ctkPluginFrameworkContext.cpp | L269-300(checkRequirePlugin) |
| 模块层 | 插件表 | ctkPlugins_p.h | L45-63 |
| 模块层 | SQLite 存储 | ctkPluginStorageSQL.cpp | L38-39 |
| 生命周期层 | 六态状态机 | ctkPlugin.h | L59-103 |
| 生命周期层 | RESOLVED 判定 | ctkPlugin_p.cpp | L174-198(getUpdatedState) |
| 生命周期层 | start0/QPluginLoader/回滚 | ctkPlugin_p.cpp | L720-790 |
| 生命周期层 | Activator 接口 | ctkPluginActivator.h | L71-111 |
| 服务层 | 服务双表 | ctkServices_p.h | L40-71 |
| 服务层 | 信号槽事件 | ctkServiceSlotEntry_p.h | L45-75 |
两种微服务的应用实例追踪
本文是《两种微服务的三层架构实现对比》的补充:各取一个真实 MITK 例子,
端到端描述具体实现过程。
- 例一(CppMicroServices):
InteractionEventObserver交互事件观察者- 例二(CTK):
IDataStorageService数据仓库服务
例一 CppMicroServices:InteractionEventObserver
us:: 微服务在 MITK 里最典型的应用:鼠标交互事件如何广播给"不认识的"观察者(十字光标同步链路中的DisplayActionEventBroadcast正是靠它接入的)。
① 定义接口(Modules/Core)
Modules/Core/include/mitkInteractionEventObserver.h:
structInteractionEventObserver{virtualvoidNotify(InteractionEvent*event,boolisHandled)=0;...};只有头文件,没有任何实现依赖——消费方和提供方都只 include 它。
② 提供方注册服务(构造时)
Modules/Core/src/Interactions/mitkDisplayActionEventBroadcast.cpp: L46-48——对象构造时把自己注册进服务表:
mitk::DisplayActionEventBroadcast::DisplayActionEventBroadcast(){us::ServiceProperties props;props["name"]=std::string("DisplayActionEventBroadcast");// 以 InteractionEventObserver 接口注册自身m_ServiceRegistration=us::GetModuleContext()->RegisterService<InteractionEventObserver>(this,props);}内部发生的事:
RegisterService→ServiceRegistry::RegisterService(usServiceRegistry.cpp: L87)- 接口名
us_service_interface_iid<InteractionEventObserver>()作为 key
写入classServices表 - 立即广播
ServiceEvent::REGISTERED - 析构时
m_ServiceRegistration.Unregister()自动注销
③ 消费方用 ServiceTracker + LDAP 过滤器查找
Modules/Core/src/Interactions/mitkDispatcher.cpp: L40-49——每个渲染窗口的事件分发器构造时建跟踪器:
// 构造 LDAP 过滤器:只要 InteractionEventObserver 这个接口的服务std::string classInteractionEventObserver="("+us::ServiceConstants::OBJECTCLASS()+"="+us_service_interface_iid<InteractionEventObserver>()+")";// 再按属性过滤:全局的 或 专属本 renderer 的us::LDAPFilterfilter("(&(|"+specificRenderer+anyRenderer+")"+classInteractionEventObserver+")");m_EventObserverTracker=newus::ServiceTracker<InteractionEventObserver>(us::GetModuleContext(),filter);LDAP 过滤器的实战用法:不只按接口,还按props里的属性筛选——同一接口的多个服务实例可以按属性精确圈定。
④ 每次事件分发时动态取服务
mitkDispatcher.cpp: L198-213——每处理一个鼠标/键盘事件:
/* Notify InteractionEventObserver */conststd::vector<us::ServiceReference<InteractionEventObserver>>listEventObserver=m_EventObserverTracker->GetServiceReferences();// 拿当前时刻的全部匹配服务for(autoit:listEventObserver){InteractionEventObserver*observer=m_EventObserverTracker->GetService(it);if(observer&&observer->IsEnabled())observer->Notify(event,eventIsHandled);// 广播}每次都现查——运行中途新加载一个带观察者的模块(比如测量工具模块注册了自己的观察者),下一个鼠标事件就会自动广播给它,Dispatcher 一行代码不用改。
完整链路
DisplayActionEventBroadcast 构造 → RegisterService<InteractionEventObserver>(this) [提供方,Modules/Core] → classServices["...InteractionEventObserver"] += 本实例 ↓ 运行期解耦 QmitkRenderWindow 收到鼠标事件 → Dispatcher::ProcessEvent → ServiceTracker(LDAP过滤).GetServiceReferences() [消费方,互不 include 实现] → observer->Notify(event) → SetCrosshair/Scroll/... → 十字光标同步链路例二 CTK:IDataStorageService
CTK 服务在 MITK 里最核心的应用:所有 View 如何拿到全局唯一的 DataStorage。
① 定义接口(带 Qt 元对象声明)
Plugins/org.mitk.core.services/src/mitkIDataStorageService.h: L29-49:
structMITK_CORE_SERVICES_PLUGINIDataStorageService{virtualIDataStorageReference::PointerGetDataStorage()const=0;virtualIDataStorageReference::PointerGetDefaultDataStorage()const=0;...};// CTK 服务必须做 Qt 接口声明——这是与 us:: 的关键差异Q_DECLARE_INTERFACE(mitk::IDataStorageService,"org.mitk.service.IDataStorageService")② 提供方在插件 Activator::start 里注册
Plugins/org.mitk.core.services/src/internal/mitkPluginActivator.cpp: L90-91——org.mitk.core.services插件被 CTK启动(进入 ACTIVE 状态)时:
voidorg_mitk_core_services_Activator::start(ctkPluginContext*context){dataStorageService.reset(newDataStorageService());// 创建实现context->registerService<mitk::IDataStorageService>(dataStorageService.data());}对比例一:us:: 的注册发生在对象构造/模块加载时;CTK 的注册发生在插件 start时——三层对比文档里"生命周期绑定点不同"在真实代码中的体现。
CTK 内部用qobject_cast校验实现确实携带Q_DECLARE_INTERFACE声明的接口,然后写入ctkServices的双表。
③ 消费方用 ctkServiceTracker
Plugins/org.mitk.gui.qt.common/src/QmitkAbstractView.cpp:
// L146: 私有类成员——跟踪器ctkServiceTracker<mitk::IDataStorageService*>m_DataStorageServiceTracker;// L51-57: 构造时打开跟踪QmitkAbstractViewPrivate(QmitkAbstractView*qq):m_DataStorageServiceTracker(QmitkCommonActivator::GetContext())// 插件上下文{m_DataStorageServiceTracker.open();// 开始跟踪(服务出现/消失自动感知)}// L390-400: 任何 View 调 GetDataStorage() 时mitk::DataStorage::PointerQmitkAbstractView::GetDataStorage()const{mitk::IDataStorageService*dsService=d->m_DataStorageServiceTracker.getService();if(dsService!=nullptr)returndsService->GetDataStorage()->GetDataStorage();returnnullptr;}④ 自动清理
- View 析构 →
m_DataStorageServiceTracker.close()→ 停止跟踪 - 插件 stop →
removePluginResources()→ 注销服务 + 清理依赖计数 - 服务注销时,所有持有引用的 View 自动收到
serviceRemoved信号,getService()返回 nullptr
完整链路
org.mitk.core.services 插件启动(ACTIVE) → Activator::start() → registerService<IDataStorageService>(dataStorageService) [提供方] → ctkServices["org.mitk.service.IDataStorageService"] += 本实例 ↓ 运行期解耦 QmitkAbstractView 构造 → ctkServiceTracker.open() → 监听服务注册/注销信号 → 任何 View 调 GetDataStorage() → tracker.getService() → DataStorageService::GetDataStorage() → 全局唯一 DataStorage 实例对比小结
| 维度 | CppMicroServices(InteractionEventObserver) | CTK(IDataStorageService) |
|---|---|---|
| 注册时机 | 对象构造时(模块加载即注册) | 插件 start 时(ACTIVE 状态才注册) |
| 接口声明 | 纯 C++ 结构体,模板元编程 | Q_DECLARE_INTERFACE+ Qt 元对象 |
| 服务发现 | ServiceTracker+LDAPFilter(属性过滤) | ctkServiceTracker(Qt 信号槽监听) |
| 生命周期绑定 | 与模块加载/卸载同步 | 与插件状态机(INSTALLED→RESOLVED→ACTIVE)同步 |
| 典型场景 | 跨模块事件广播(鼠标交互) | 插件间共享单例(全局 DataStorage) |
这两个实例完整展示了三层架构对比在 MITK 中的实际应用:us:: 用于模块层的轻量事件广播,CTK 用于插件层的完整生命周期管理 + 服务共享。
