两种微服务的三层架构实现对比
按 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)
cpp
void ctkPluginFrameworkContext::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; // 正在本轮解析中(处理循环依赖)
else if (RESOLVED_FLAGS & p2->state) ok = p2; // 已解析
else if (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):
cpp
ctkPluginException* ctkPluginPrivate::start0()
{
fwCtx->listeners.emitPluginChanged(ctkPluginEvent(STARTING, ...));
pluginLoader.load(); // QPluginLoader 加载库
pluginActivator = qobject_cast<ctkPluginActivator*>(
pluginLoader.instance()); // Qt 插件根对象即 Activator
pluginActivator->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):
cpp
class ctkPluginActivator {
virtual void start(ctkPluginContext* context) = 0;
virtual void stop(ctkPluginContext* context) = 0;
};
CppMicroServices:二态(usModule.h: L64,166)
未加载 ⇄ 已加载(IsLoaded() 一个 bool)
- 加载即注册即启动 :静态初始化(
US_INITIALIZE_MODULE)→
ModuleRegistry::Register→Module::Start一气呵成,没有中间可停留的状态 - Activator 获取用原生符号查找 (
usModule.cpp: L133-135):
cpp
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:
cpp
struct InteractionEventObserver {
virtual void Notify(InteractionEvent* event, bool isHandled) = 0;
...
};
只有头文件,没有任何实现依赖------消费方和提供方都只 include 它。
② 提供方注册服务(构造时)
Modules/Core/src/Interactions/mitkDisplayActionEventBroadcast.cpp: L46-48------对象构造时把自己注册进服务表:
cpp
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------每个渲染窗口的事件分发器构造时建跟踪器:
cpp
// 构造 LDAP 过滤器:只要 InteractionEventObserver 这个接口的服务
std::string classInteractionEventObserver =
"(" + us::ServiceConstants::OBJECTCLASS() + "="
+ us_service_interface_iid<InteractionEventObserver>() + ")";
// 再按属性过滤:全局的 或 专属本 renderer 的
us::LDAPFilter filter("(&(|" + specificRenderer + anyRenderer + ")"
+ classInteractionEventObserver + ")");
m_EventObserverTracker =
new us::ServiceTracker<InteractionEventObserver>(us::GetModuleContext(), filter);
LDAP 过滤器的实战用法:不只按接口,还按 props 里的属性筛选------同一接口的多个服务实例可以按属性精确圈定。
④ 每次事件分发时动态取服务
mitkDispatcher.cpp: L198-213------每处理一个鼠标/键盘事件:
cpp
/* Notify InteractionEventObserver */
const std::vector<us::ServiceReference<InteractionEventObserver>> listEventObserver =
m_EventObserverTracker->GetServiceReferences(); // 拿当前时刻的全部匹配服务
for (auto it : 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:
cpp
struct MITK_CORE_SERVICES_PLUGIN IDataStorageService {
virtual IDataStorageReference::Pointer GetDataStorage() const = 0;
virtual IDataStorageReference::Pointer GetDefaultDataStorage() 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 状态)时:
cpp
void org_mitk_core_services_Activator::start(ctkPluginContext* context)
{
dataStorageService.reset(new DataStorageService()); // 创建实现
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:
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::Pointer QmitkAbstractView::GetDataStorage() const
{
mitk::IDataStorageService* dsService = d->m_DataStorageServiceTracker.getService();
if (dsService != nullptr)
return dsService->GetDataStorage()->GetDataStorage();
return nullptr;
}
④ 自动清理
- 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 用于插件层的完整生命周期管理 + 服务共享。