MITK中微服务三套注册表数据结构分析

MITK 三套注册表数据结构分析

第一部分:BlueBerry 扩展注册表(ExtensionRegistry / RegistryObjectManager)

第二部分:CppMicroServices 注册表(ModuleRegistry / ServiceRegistry)

第三部分:CTK 中的注册表(ctkPlugins / ctkServices)

第四部分:血缘关系 ------ Knopflerfish 与 Equinox

附:CppMicroServices 版本信息


第一部分 BlueBerry 扩展注册表的数据结构

源码:Plugins/org.blueberry.core.runtime/src/internal/,核心是 RegistryObjectManager

一、总体设计:int ID 扁平对象图 + 多索引

注册表不是一棵指针连接的对象树,而是:

所有对象压平存储,以 int objectId 为主键;父子关系用 int 数组表达;
再配多张字符串索引表加速查询。

复制代码
                    ┌─────────────────────────────────────────┐
                    │        RegistryObjectManager            │
   字符串索引        │  extensionPoints: QHash<QString,int>    │──┐
   (名字→ID)       │  namespacesIndex: KeyedHashSet          │  │ 查到 id
                    │  contributors:  QHash<QString,Contrib>  │  │
                    │  orphanExtensions: QHash<QString,QList<int>>│
                    ├─────────────────────────────────────────┤  │
   主存储            │  cache: RegistryObjectReferenceMap      │◄─┘
   (ID→对象)       │        (int → RegistryObject, 可软引用)  │
                    │  heldObjects: KeyedHashSet (强引用锚)    │
                    └─────────────────────────────────────────┘

二、对象本体:RegistryObject(berryRegistryObject.h: L26

三类对象(ExtensionPoint / Extension / ConfigurationElement)共享同一基类:

cpp 复制代码
class RegistryObject : public KeyedElement {
  uint objectId;            // 主键(nextId 自增分配)
  QList<int> children;      // ★ 子对象只存 ID 数组,不存指针
  int extraDataOffset;      // 位打包:bit31=无额外数据, bit30=持久化标志,
                            //         bit0-29=缓存文件偏移(≤1GB)
};

两个关键决策:

  1. childrenQList<int> ------树结构完全靠 ID 间接引用。
    好处:对象可独立序列化到磁盘缓存、可懒加载、删除时无悬空指针。
  2. extraDataOffset 位打包------一个 int 同时编码"持久化标志 + 缓存文件偏移"。

ConfigurationElement 的属性存法berryConfigurationElement.h: L36-40)极致紧凑:

cpp 复制代码
// 格式: [p1, v1, p2, v2, ..., configurationElementValue]
// 偶数长度 = 没有元素文本值;属性名/值交替排列
QList<QString> propertiesAndValue;   // 不用 QHash!一个扁平字符串数组

每元素通常只有 2~4 个属性,线性扫比哈希省内存。

三、主存储:RegistryObjectReferenceMap(ID → 对象)

berryRegistryObjectManager.h: L208

cpp 复制代码
RegistryObjectReferenceMap* cache;   // key: object id, value: 对象
KeyedHashSet heldObjects;            // 必须常驻内存对象的强引用

RegistryObjectReferenceMap 支持 HARD/SOFT 两种引用模式

berryRegistryObjectReferenceMap.h: L32-37)------软引用模式下不常用的对象

可被回收,需要时从磁盘缓存按 extraDataOffset 重新加载(Load(id, type))。

Add(object, hold)hold 参数决定是否放进 heldObjects 锚定。

四、索引表:按不同维度加速查询

索引 类型 用途
extensionPoints HashtableOfStringAndIntQHash<QString,int> 薄封装) 扩展点全名 → objectId
namespacesIndex KeyedHashSet(内部 QSet<KeyedElement>,元素自带 GetKey) 命名空间 → 该插件的 extension/extensionPoint 集合
contributors QHash<QString, RegistryContributor> 贡献者 ID → 贡献者(哪个插件)
orphanExtensions QHash<QString, QList<int>> 孤儿表:目标扩展点未注册的 extension 暂存,等扩展点出现再认领
newContributions / formerContributions KeyedHashSet 本次会话新增贡献 vs 上次会话缓存贡献(懒加载)

五、对外暴露:Handle 层(安全间接引用)

消费者从来拿不到 RegistryObject 裸指针,拿到的是 HandleberryHandle.h: L31):

cpp 复制代码
class Handle : public virtual Object {
  const IObjectManager* const objectManager;
  int id;                              // Handle 只持有 ID!
  virtual RegistryObject* GetObject(); // 每次访问按 ID 现查
};

IConfigurationElement / IExtension / IExtensionPoint 的实现全是

"ID + 管理器指针"的轻量句柄。好处:插件卸载、对象移除后,

Handle 访问得到受控失败而非野指针崩溃;Handle 可随意复制,代价只是一个 int。

六、线程安全与懒加载

  • 所有公开方法加 QMutex mutex(L105),内部用 _unlocked 后缀版本避免重入死锁
  • 多个 xxxLoaded 布尔标志(orphanExtensionsLoadedcontributorsLoaded
    namespacesIndexLoaded)实现分区懒加载------哪部分被查询才加载哪部分
  • isDirty / fromCache 标志支撑"注册表磁盘缓存":
    启动时间戳一致则直接映射缓存文件,跳过全部 plugin.xml 解析

七、查询路径示例

复制代码
GetConfigurationElementsFor("org.blueberry.ui.views")
  → extensionPoints 索引查到扩展点 objectId
  → cache.Get(id) 取 ExtensionPoint 对象(可能触发磁盘 Load)
  → GetRawChildren() 拿到 extension 的 int ID 数组
  → 逐个 GetObjects(ids, EXTENSION) → 再取各自 children(ConfigurationElement ID)
  → 包装成 ConfigurationElementHandle 列表返回

小结

设计点 实现
主键化 一切对象 = int objectId,nextId 自增
树 = ID 数组 children: QList<int>,无指针互连
属性 = 扁平数组 [p1,v1,p2,v2,...,value] 交替存放
内存可伸缩 ReferenceMap 软引用 + heldObjects 强引用锚 + 磁盘偏移量重加载
多维索引 名字/命名空间/贡献者/孤儿四套哈希索引
安全暴露 Handle(ID+管理器)而非裸指针
并发 单 QMutex + _unlocked 内部版本
启动加速 位打包偏移 + 分区懒加载 + 时间戳校验缓存

这套结构原样照搬 Eclipse Equinox------为"数千插件、数万扩展"的规模优化:

启动不解析、内存可回收、查询走索引


第二部分 CppMicroServices 注册表的数据结构

与 BlueBerry 完全不同的设计------纯内存、指针直连、按需求即时索引 的三层哈希结构。

源码:Modules/CppMicroServices/core/src/

一、三张注册表总览

复制代码
① ModuleRegistry(模块表,全局静态)    QHash: 模块名 → Module*
② ServiceRegistry(服务表,挂在 CoreModuleContext 里)
     三个容器互为视角,指向同一批 ServiceRegistrationBase
③ ServiceListeners(监听器表)          按过滤条件分桶的监听器缓存

二、ModuleRegistry ------ 模块表(module/usModuleRegistry.cpp: L39-73

cpp 复制代码
typedef US_UNORDERED_MAP_TYPE<std::string, Module*> ModuleMap;   // name → Module*
US_GLOBAL_STATIC_WITH_DELETER(ModuleMap, modules, ModuleDeleter)  // 进程级静态单例
US_GLOBAL_STATIC(Mutex, modulesLock)                              // 独立互斥锁
US_GLOBAL_STATIC(Mutex, countLock)                                // 保护自增 id 计数

一张 unordered_map<string, Module*>裸指针直连

(对象生命周期与 .so 绑定,进程退出时由 ModuleDeleter 统一清理)。

三、ServiceRegistry ------ 服务表核心(service/usServiceRegistry_p.h: L63-82

cpp 复制代码
class ServiceRegistry {
  mutable Mutex mutex;   // 单锁保护全部三个容器

  // 视角1: 服务注册对象 → 它声明的接口名列表
  US_UNORDERED_MAP_TYPE<ServiceRegistrationBase, std::vector<std::string>> services;

  // 视角2: 接口名 → 按 ranking 升序的注册列表(★ 查询主索引)
  US_UNORDERED_MAP_TYPE<std::string, std::vector<ServiceRegistrationBase>> classServices;

  // 视角3: 全量平面列表(按模块枚举时用)
  std::vector<ServiceRegistrationBase> serviceRegistrations;
};

三个容器存同一批 ServiceRegistrationBase(引用计数句柄),索引维度不同:

  • classServices 是查询热路径:GetServiceReference("IAlgorithm") 一次哈希命中,
    back() 即最高 ranking(插入时 lower_bound 保序,usServiceRegistry.cpp: L121-124
  • 一个服务注册 N 个接口,就在 classServices 出现 N 次

四、服务对象本体:ServiceRegistrationBasePrivate(usServiceRegistrationBasePrivate.h: L40-115

cpp 复制代码
class ServiceRegistrationBasePrivate {
  AtomicInt ref;                    // 隐式共享的引用计数(handle/body 惯用法)

  InterfaceMap service;             // ★ 服务本体: std::map<string接口名, void*实现指针>
                                    //   (usServiceInterface.h: L138)

  // ---- 使用者记账(与 BlueBerry 最大的不同点)----
  ModuleToRefsMap dependents;       // QHash<Module*,int>: 谁在用我 + 未配对的 GetService 次数
  ModuleToServiceMap moduleServiceInstance;      // 模块作用域工厂: 每模块一个实例
  ModuleToServicesMap prototypeServiceInstances; // 原型工厂: 每模块多个实例

  ModulePrivate* module;            // 谁注册的我(裸指针回连)
  ServiceReferenceBase reference;   // 对外发放的引用对象
  ServicePropertiesImpl properties; // 属性表(service.id / objectclass / service.ranking)

  volatile bool available;          // 服务是否可获取
  volatile bool unregistering;      // 防递归注销标志
  Mutex eventLock, propsLock;       // 细粒度锁
};

关键设计:

  1. InterfaceMap = std::map<std::string, void*> ------服务本体就是
    "接口名 → 对象指针"的 map。C++ 无反射,多接口注册靠模板在编译期
    T*void* 存入,取出时按接口名转回。
  2. dependents 使用计数 ------每次 GetService +1、UngetService -1;
    模块卸载时框架据此知道"谁还欠着我的服务没还",实现自动清理。
  3. handle/body 分离 ------对外的 ServiceRegistrationBase / ServiceReferenceBase
    都是指向 Private 的引用计数薄壳;服务注销后 Private 仍可存活
    (available=false),旧引用访问得到受控失败。

五、ServiceListeners ------ 监听器表(service/usServiceListeners_p.h: L43-65

过滤器优化的两级结构:

cpp 复制代码
class ServiceListeners {
  // 简单过滤器(只按 objectclass 匹配)→ 缓存桶,事件分发 O(1) 定位
  US_UNORDERED_MAP_TYPE<std::string, std::list<ServiceListenerEntry>> cache[2];
  //   按 objectclass 和 service.id 两个维度各一份

  // 复杂 LDAP 过滤器 / 无过滤器 → 只能逐个求值
  std::list<ServiceListenerEntry> complicatedListeners;

  // 模块级监听器:ModuleContext → 回调列表
  US_UNORDERED_MAP_TYPE<ModuleContext*, std::list<std::pair<ModuleListener,void*>>> moduleListenerMap;
};

发 ServiceEvent 时:先按事件服务的 objectclass/service.id 命中缓存桶里的简单监听器,

再对 complicatedListeners 逐个跑 LDAP 表达式------避免每次事件全量遍历。


第三部分 CTK 中的注册表

CTK 源码位置:D:\MITK-superbuild-2023.12\ep\src\CTK\Libs\PluginFramework\

(CTK 是外部依赖,由 CMakeExternals/CTK.cmakegithub.com/MITK/CTK 拉取)

结论先行:

CTK 有两张注册表(插件表 + 服务表),但没有 ExtensionRegistry。

BlueBerry ExtensionRegistry 不是"依赖 CTK 实现"的------它的解析与存储是

BlueBerry 自有代码,CTK 只提供输入源和触发时机(详见下文第三节)。

一、CTK 的两张注册表

1. ctkPlugins ------ 插件注册表(ctkPlugins_p.h: L45-63

cpp 复制代码
class ctkPlugins {
  QHash<QString, QSharedPointer<ctkPlugin>> plugins;  // location(URL) → 插件
  mutable QReadWriteLock pluginsLock;                 // 读写锁(比 us:: 的单 Mutex 更细)
};

2. ctkServices ------ 服务注册表(ctkServices_p.h: L40-71

cpp 复制代码
class ctkServices {
  QHash<ctkServiceRegistration, QStringList> services;          // 注册 → 接口名列表
  QHash<QString, QList<ctkServiceRegistration>> classServices;  // 接口名 → 按rank排序的注册列表
};

这与第二部分 CppMicroServices 的 ServiceRegistry 几乎逐字段对应

services / classServices 连名字都一样)------因为两者同源(见第四部分),

只是 CTK 用 Qt 容器 + QSharedPointer,us:: 用 STL + 自制引用计数。

二、CTK 独有的两个特性

1. SQLite 持久化(ctkPluginStorageSQL

us:: 和 BlueBerry 都没有的能力:

cpp 复制代码
// ctkPluginStorageSQL.cpp: L38-39
#define PLUGINS_TABLE          "Plugins"           // 插件元数据表
#define PLUGIN_RESOURCES_TABLE "PluginResources"   // ★ 插件资源(含 plugin.xml)存进 SQLite!

CTK 把插件安装状态、以及插件内嵌资源整个存入 SQLite 数据库

plugin->getResource("plugin.xml") 实际是从这个数据库读出来的。

2. Qt 信号槽做服务事件(ctkServiceSlotEntry

监听器不是回调接口而是 Qt slot,配 LDAP 过滤器(ctkLDAPExpr,同样来自 Knopflerfish)。

三、修正:"BlueBerry ExtensionRegistry 依赖 CTK 实现"

准确的关系是分层协作,而非实现依赖

环节 谁做的
plugin.xml 字节从哪来 CTKplugin->getResource() 从 SQLite 资源表取出
何时触发解析 CTK :插件 RESOLVED 事件 → CTKPluginListener::PluginChanged
ExtensionRegistry 注册为服务 CTKcontext->registerService<IExtensionRegistry>()
XML 解析 BlueBerry 自己ExtensionsParser(SAX 状态机)
存储结构 BlueBerry 自己RegistryObjectManager(第一部分详析)
查询/Handle API BlueBerry 自己

所以:CTK 是 ExtensionRegistry 的"宿主与快递员" (管插件生命周期、

送来 plugin.xml 字节),但注册表本身的解析、存储、查询全部是 BlueBerry

从 Eclipse Equinox 移植的自有代码。CTK 自己压根没有扩展点概念

Libs/ 下搜不到 ExtensionRegistry,只有无关的 ctkExtensionFactory

------那是 Qt Designer 插件工厂)。


第四部分 血缘关系:Knopflerfish 与 Equinox

Knopflerfish 是什么

Knopflerfish 是一个开源的 Java OSGi 框架实现 ------理解它是什么,

就能理解为什么 CTK 和 CppMicroServices 的代码长得几乎一样。

  • 由瑞典公司 Makewave(前身 Gatespace Telematics)开发维护,2003 年开源,BSD 风格许可证
  • 是 OSGi 规范最早、最著名的三大实现之一:
OSGi 实现 维护方 著名衍生物
Equinox Eclipse 基金会 Eclipse IDE 的内核 → BlueBerry 移植自它
Felix Apache 众多 Java 服务器
Knopflerfish Makewave CTK PluginFrameworkCppMicroServices 移植自它

OSGi 是什么(一句话背景)

OSGi = Java 世界的动态模块化规范:程序由多个 bundle (模块)组成,

bundle 可在运行期安装/启动/停止/卸载,bundle 之间通过服务注册表

以接口解耦通信。"动态服务注册 + 模块化系统"这两个概念就是 OSGi 规范的核心内容。

与 MITK 的血缘关系

复制代码
Knopflerfish(Java, OSGi 实现)
    │
    ├─ CTK PluginFramework(把 Java 代码手工翻译成 Qt/C++)
    │     ctkServices、ctkLDAPExpr、ctkServiceSlotEntry ...
    │
    └─ CppMicroServices(翻译成纯 STL C++)
          usServiceRegistry、usLDAPExpr、usServiceListenerEntry ...

翻译痕迹在源码里非常明显:

  • 类名逐一对应:Java 的 ServicesctkServices / us::ServiceRegistry
    LDAPExprctkLDAPExpr / usLDAPExpr
  • 数据结构逐字段对应:services / classServices 两张表的名字在三份代码里一模一样
  • 连注释和算法(如 LDAP 过滤器求值、service ranking 排序)都是同一套

BlueBerry 走的是另一条线 :它移植的是 Equinox (Eclipse 的 OSGi 实现)

里的扩展注册表和 Workbench,所以 ExtensionRegistry / RegistryObjectManager

的设计(int ID 扁平图、磁盘缓存、孤儿表)与 Knopflerfish 系毫无相似之处

------那是 Eclipse 的家传手艺。

MITK 里"OSGi 味"的代码其实有两个不同的 Java 祖先:

两套微服务机制(CTK 服务层、CppMicroServices)是 Knopflerfish 的 C++ 后代;

BlueBerry 的扩展点机制则来自 Eclipse Equinox。


第五部分 三套注册表全景对照

CTK ctkServices/ctkPlugins CppMicroServices ServiceRegistry BlueBerry ExtensionRegistry
血统 Knopflerfish OSGi (Java→Qt) Knopflerfish OSGi (Java→STL) Eclipse Equinox (Java→Qt)
管什么 插件 + 服务(活对象) 模块 + 服务(活对象) 扩展声明(静态元数据)
主键 接口名字符串 / location 接口名字符串 / 模块名 int objectId
核心结构 QHash 双表 unordered_map 双表 int ID 扁平图 + 多索引
对象互连 QSharedPointer 裸指针/引用计数句柄 int ID 数组间接引用
持久化 SQLite(含资源) 自制二进制缓存 + 偏移量
记账重点 谁在用(dependents) 谁在用(dependents) 谁贡献的(contributor 归属)
事件 Qt 信号槽 + LDAP 回调 + LDAP IRegistryEventListener
QReadWriteLock 单 Mutex 单 QMutex
规模假设 数十插件 数百服务,查询要快 数万扩展,启动要快
典型查询 接口名 → 服务 接口名 → 最高 ranking 实例 扩展点 ID → 配置元素树

本质差异一句话:Service 类注册表管"运行中的对象"(所以要引用计数、
使用记账、可用性标志),ExtensionRegistry 管"静态的声明"(所以要 ID 化、
可序列化、懒加载)
------分别对应 OSGi 的 Service Layer 和 Eclipse 的

Extension Registry。三者在 MITK 进程里同时运行:CTK 管 Plugins 层生命周期

与框架服务,us:: 管 Modules 层算法服务,BlueBerry ExtensionRegistry

管 UI 扩展声明------各司其职。


附 CppMicroServices 版本信息

版本 2.99.0 (内嵌定制版)。来源:Modules/CppMicroServices/CMakeLists.txt

cmake 复制代码
set(${PROJECT_NAME}_MAJOR_VERSION 2)
set(${PROJECT_NAME}_MINOR_VERSION 99)
set(${PROJECT_NAME}_PATCH_VERSION 0)
  • 2.99.0 是"2.x 向 3.0 过渡"的开发版本号 (99 是惯用的预发布标记)。
    不是上游正式 release,而是 MITK fork 进源码树内嵌维护 的版本------
    所以它放在 Modules/CppMicroServices/ 而不是 CMakeExternals/ 作为外部依赖。
  • API 特征印证:仍使用 2.x 时代术语 ------us:: 命名空间、
    Module/ModuleContext/US_INITIALIZE_MODULE(基于 OSGi Core Release 5 规范)。
  • 上游独立项目(github.com/CppMicroServices/CppMicroServices)3.x 之后改成
    cppmicroservices:: 命名空间和 Bundle/BundleContext 术语,
    MITK 内嵌版没有跟进,由 MITK 团队(DKFZ)随主仓库维护。

关键源码文件索引

BlueBerry(Plugins/org.blueberry.core.runtime/src/internal/ 下)

内容 文件 关键行
对象管理器 berryRegistryObjectManager.h L203-239(全部私有字段)
对象基类 berryRegistryObject.h L26-100
配置元素属性 berryConfigurationElement.h L36-49
软/硬引用 Map berryRegistryObjectReferenceMap.h L32-70
字符串→int 表 berryHashtableOfStringAndInt.h L20
KeyedHashSet berryKeyedHashSet.h / berryKeyedElement.h -
Handle 基类 berryHandle.h L31-56

CppMicroServices(Modules/CppMicroServices/core/src/ 下)

内容 文件 关键行
模块表 module/usModuleRegistry.cpp L39-73
服务表 service/usServiceRegistry_p.h L63-82
服务注册私有体 service/usServiceRegistrationBasePrivate.h L40-115
InterfaceMap 定义 ../include/usServiceInterface.h L138
监听器表 service/usServiceListeners_p.h L43-65
版本号 ../CMakeLists.txt MAJOR/MINOR/PATCH_VERSION

CTK(ep/src/CTK/Libs/PluginFramework/ 下)

内容 文件 关键行
CTK 外部依赖声明 D:/MITK/CMakeExternals/CTK.cmake L57-58(GIT_REPOSITORY / GIT_TAG)
插件注册表 ctkPlugins_p.h L45-63
服务注册表 ctkServices_p.h L40-71
SQLite 持久化 ctkPluginStorageSQL_p.h/.cpp cpp L38-39(表名)
服务事件槽 ctkServiceSlotEntry_p.h L45-75
LDAP 过滤器 ctkLDAPExpr_p.h -
相关推荐
Wang's Blog6 小时前
Go-Zero项目开发34: 微服务超时控制与重试机制实践
开发语言·微服务·golang·go-zero
X-⃢_⃢-X7 小时前
四、OpenFeign远程调用
java·微服务·springboot·springcloud
beibeix20159 小时前
MITK中两种微服务的三层架构实现对比
微服务·mitk
在水一缸10 小时前
深入浅出:Node.js 下一代 ORM 架构设计与实战解析
数据库·微服务·云原生·node.js·orm·架构设计
Wang's Blog10 小时前
Go-Zero项目开发30: 微服务配置中心的设计与实现
开发语言·微服务·golang·go-zero
小罗水11 小时前
第3章 从单体项目到 Spring Cloud 微服务
spring·spring cloud·微服务
Wang's Blog13 小时前
Go-Zero项目开发33: IM实时通信前后端对接与微服务基础设施实践
开发语言·微服务·golang·go-zero
beibeix201513 小时前
MITK中org.blueberry.ui.qt 插件功能分析
mitk
啊啊啊迈 旋棍1 天前
使用Istio治理微服务入门
微服务·云原生·istio