|
1 | | -# 动态模型设计 |
| 1 | +以下你觉得怎么样 |
| 2 | +# Nop平台动态模型加载与多租户支持架构设计 |
2 | 3 |
|
3 | | -## 整体设计 |
| 4 | +## 设计概览 |
4 | 5 |
|
5 | | -整个设计思想如下: |
| 6 | +Nop平台采用统一的动态模型加载机制,支持多租户环境下的资源隔离。核心设计基于"Loader as Generator"模式:`Model = Loader(virtualPath × tenantId)`,通过标准化的Provider接口实现模型的按需加载和缓存管理。 |
6 | 7 |
|
7 | | -```javascript |
| 8 | +## 核心架构 |
| 9 | + |
| 10 | +### 组件职责边界 |
| 11 | + |
| 12 | +| 组件 | 职责 | 关键方法 | |
| 13 | +|---------------------------------|------------------------|-----------------------------------------------| |
| 14 | +| **IDynamicResourceProvider** | 虚拟路径 → 模型文件资源 | `getResource(virtualPath)` | |
| 15 | +| **IDynamicBizModelProvider** | 业务对象名 → XMeta/XBiz模型路径 | `getBizModel(bizObjName)`, `getBizObjNames()` | |
| 16 | +| **IOrmModelProvider** | 加载ORM模型 | `getOrmModel(persistEnv)` | |
| 17 | +| **IDynamicEntityModelProvider** | 实体名 → 实体模型 | `getEntityModel(entityName)` | |
| 18 | +| **IDynamicModuleDiscovery** | 发现可用模块 | `getEnabledModules()` | |
| 19 | + |
| 20 | +通过依赖注入获取这些接口的实现。 |
| 21 | + |
| 22 | +### 核心访问模式 |
| 23 | + |
| 24 | +```java |
| 25 | +// 统一业务访问接口 |
8 | 26 | IBizObject bizObj = bizObjectManager.getBizObject(bizObjName); |
9 | 27 | bizObj.invoke(actionName, request, selection, svcCtxt); |
10 | 28 | ``` |
11 | 29 |
|
12 | | -1. 业务层面总是按照业务对象名来访问后台 |
13 | | -2. BizObjectManager根据bizObjName加载IBizObject。如果在内存中没有找到,则会通过IDynamicBizModelProvider去动态加载对象模型GraphQLBizModel, 根据它创建BizObjectImpl |
14 | | -3. GraphQLBizModel内部会用到bizPath和metaPath,它们对应于XBiz模型文件和XMeta文件的虚拟文件路径。 |
15 | | -4. ResourceComponentManager.loadComponentModel(modelPath) 加载XBiz和XMeta模型文件的时候会调用 IDynamicResourceProvider去获取动态IResource对象。 |
16 | | -5. action内部需要访问数据库时会通过OrmSessionFactory获取OrmSession,此时会通过IOrmModelProvider获取OrmModel。 |
17 | | -6. 返回的OrmModel内部使用IDynamicEntityModelProvider来根据实体名来动态加载单个实体的EntityModel。 |
| 30 | +## 动态加载机制 |
| 31 | + |
| 32 | +### 加载流程链 |
| 33 | + |
| 34 | +```text |
| 35 | +业务请求 → BizObjectManager → IDynamicBizModelProvider → 模型路径 → IDynamicResourceProvider → 资源内容 |
| 36 | + ↓ |
| 37 | +数据库访问 → OrmSessionFactory → IOrmModelProvider → LazyLoadOrmModel → entityName → IDynamicEntityModelProvider → IEntityModel |
| 38 | +``` |
| 39 | + |
| 40 | +### 详细加载过程 |
| 41 | + |
| 42 | +1. **业务对象加载** |
| 43 | + - `BizObjectManager` 根据 `bizObjName` 查找缓存 |
| 44 | + - 未命中时通过 `IDynamicBizModelProvider` 加载 `GraphQLBizModel` |
| 45 | + - 根据返回的 `bizPath` 和 `metaPath` 加载模型文件 |
| 46 | + - `ResourceComponentManager` 调用 `IDynamicResourceProvider` 获取资源内容 |
| 47 | + |
| 48 | +2. **数据库访问** |
| 49 | + - Action中通过 `OrmSessionFactory` 获取会话 |
| 50 | + - 通过 `IOrmModelProvider` 动态获取实体ORM模型 |
| 51 | + - 支持按模块粒度加载ORM模型 |
| 52 | + |
| 53 | +### 加载粒度策略 |
| 54 | + |
| 55 | +| 粒度级别 | 描述 | 触发条件 | |
| 56 | +|----------|------|----------| |
| 57 | +| **资源文件级** | 单个IResource资源文件 | `IDynamicResourceProvider.getResource()` | |
| 58 | +| **业务对象级** | XMeta + XBiz模型文件对 | `BizObjectManager.getBizObject()` | |
| 59 | +| **模块级** | 一组相关业务对象及ORM模型 | 访问模块内任何资源 | |
| 60 | + |
| 61 | +**关键特性**: |
| 62 | +- 最小加载粒度为单个资源文件 |
| 63 | +- 所有加载操作均为Lazy模式 |
| 64 | +- 动态加载器作为最后回退方案 |
| 65 | + |
| 66 | +## 多租户支持 |
| 67 | + |
| 68 | +### 租户隔离架构 |
| 69 | + |
| 70 | +```java |
| 71 | +// 租户感知的缓存层次结构 |
| 72 | +TenantAwareResourceLoadingCache |
| 73 | +├── 租户缓存容器 (tenantCaches) |
| 74 | +│ └── 各租户独立的ResourceLoadingCache |
| 75 | +└── 共享缓存 (shareCache) |
| 76 | +``` |
| 77 | + |
| 78 | +### 缓存管理 |
| 79 | + |
| 80 | +**统一缓存接口**: |
| 81 | +```java |
| 82 | +public interface ICacheManagement<K> { |
| 83 | + String getName(); |
| 84 | + void remove(@Nonnull K key); |
| 85 | + void clear(); |
| 86 | + default void clearForTenant(String tenantId) {} |
| 87 | +} |
| 88 | +``` |
| 89 | + |
| 90 | +**租户缓存路由逻辑**: |
| 91 | +```java |
| 92 | +protected ResourceLoadingCache<V> getCache(String path) { |
| 93 | + String tenantId = getTenantId(); |
| 94 | + if (StringHelper.isEmpty(tenantId) || !ResourceTenantManager.supportTenant(path)) { |
| 95 | + return shareCache; // 无租户或路径不支持租户 |
| 96 | + } |
| 97 | + return tenantCaches.get(tenantId); // 租户特定缓存 |
| 98 | +} |
| 99 | +``` |
| 100 | + |
| 101 | +### 生命周期管理 |
| 102 | + |
| 103 | +- **租户会话期**:租户特定缓存自动维护 |
| 104 | +- **资源回收**:LRU策略自动清理最少使用条目 |
| 105 | +- **主动清理**:支持按租户粒度手动清理 |
| 106 | +- **状态特性**:所有缓存对象无状态,可随时重建 |
| 107 | + |
| 108 | +## 模块化架构 |
| 109 | + |
| 110 | +### 模块初始化 |
| 111 | + |
| 112 | +```text |
| 113 | +// 无状态模块初始化流程 |
| 114 | +模块访问 → 执行模块初始化 → 生成模型文件 → 注册监听器 |
| 115 | +``` |
| 116 | +- 基于模板 `/nop/templates/dyn-module` 动态生成 |
| 117 | +- 完全无状态,无初始化顺序依赖 |
| 118 | +- 按需触发,避免不必要的初始化开销 |
| 119 | + |
| 120 | +### 模块间协作 |
| 121 | + |
| 122 | +| 协作方式 | 机制 | 特点 | |
| 123 | +|----------|------|------| |
| 124 | +| **服务调用** | 通过IBizObject接口 | 松耦合,运行时发现 | |
| 125 | +| **事件通信** | 消息总线 + 监听器 | 解耦,异步处理 | |
| 126 | +| **数据共享** | 通过标准业务接口 | 避免直接数据访问 | |
| 127 | + |
| 128 | +**监听器初始化**:首次需要事件处理时,搜集所有启用模块的监听器,触发相关模块初始化。 |
| 129 | + |
| 130 | +## 系统约束与保证 |
| 131 | + |
| 132 | +### 资源发现约束 |
| 133 | + |
| 134 | +- **禁止大规模扫描**:仅两个接口支持遍历语义 |
| 135 | +- **精确路径访问**:严格按名称和路径获取资源 |
| 136 | +- **最小影响范围**:动态变更的影响范围可控 |
| 137 | + |
| 138 | +### 依赖管理 |
| 139 | + |
| 140 | +- **禁止循环依赖**:模型加载时检测并报错 |
| 141 | +- **显式依赖**:通过标准接口声明依赖关系 |
| 142 | + |
| 143 | +### 并发安全 |
18 | 144 |
|
19 | | -最小的动态加载粒度是单个IResource资源文件。IDynamicResourceProvider加载的时候会根据虚拟文件路径动态决定如何生成一个InMemoryTextResource。 |
| 145 | +- **无状态模型**:纯逻辑结构,无状态迁移问题 |
| 146 | +- **线程安全缓存**:LoadingCache + Double Check锁定 |
| 147 | +- **分布式同步**:消息总线通知缓存失效 |
20 | 148 |
|
21 | | -对于动态生成的BizObject,它实际上对应于两个资源文件,XMeta和XBiz。IDynamicBizModelProvider本质上只是根据对象名来动态确定两个路径。 |
| 149 | +## 错误处理策略 |
22 | 150 |
|
23 | | -每个对象属于一个模块Module。访问该模块的任何资源文件会导致自动执行该模块的动态初始化代码。这可能导致预先生成一批InMemoryTextResource。原先加载一批代码等。 |
24 | | -比如加载模块的orm-interceptor监听器。 |
| 151 | +### 异常处理原则 |
25 | 152 |
|
26 | | -有些资源的实际加载粒度是模块。比如OrmModel。按照实体名去动态加载实体模型时,会动态确定它是哪个模块的。然后一次性加载该模块的OrmModel,然后把它合并到当前的OrmModel中。 |
| 153 | +- **快速失败**:资源未找到或加载失败立即抛出异常 |
| 154 | +- **统一异常**:遵循NopException规范 |
| 155 | +- **可扩展降级**:通过替换Loader实现定制策略 |
27 | 156 |
|
28 | | -所有加载都是Lazy的,也就是说如果访问了一个模块的BizObject,但是如果没有用到该模块的ORM,则实际不会去加载这个模块的实体模型。 |
| 157 | +### 典型错误场景 |
29 | 158 |
|
30 | | -动态加载器总是最后使用。也就是说如果在缓存中或者当前虚拟文件系统中已经存在,则返回已经存在的资源。 |
| 159 | +| 场景 | 处理方式 | 恢复策略 | |
| 160 | +|------|----------|----------| |
| 161 | +| 模型文件未找到 | 抛出NopException | 检查路径配置 | |
| 162 | +| 加载过程异常 | 直接抛出异常 | 修复底层问题 | |
| 163 | +| 租户资源隔离 | 租户特定异常 | 检查租户权限 | |
31 | 164 |
|
32 | | -核心逻辑如下: |
33 | | -1. virtualPath => DynamicGeneratedResource |
34 | | -2. bizObjName => virtualPath |
35 | | -3. entityName => moduleName => DynamicGeneratedOrmModel |
36 | | -4. virtualPath => moduleName => Init Module Once |
| 165 | +## 性能优化 |
37 | 166 |
|
38 | | -IDynamicBizModelProvider.getBizObjNames() 提供所有BizObjName名称,包含所有未加载的业务对象的名称 |
| 167 | +### 缓存策略 |
39 | 168 |
|
40 | | -IDynamicModuleDiscovery.getEnabledModules() 返回所有可用的模块,但是实际访问之前并没有加载。 |
| 169 | +- **多级缓存**:租户隔离 + 共享缓存 |
| 170 | +- **智能回收**:基于LRU的自动内存管理 |
| 171 | +- **分布式同步**:跨节点缓存一致性 |
41 | 172 |
|
42 | | -也就是说存在三种粒度: |
43 | | -1. 单个资源文件(但是解析或者生成资源文件的过程中可能会引入更多的资源文件依赖) |
44 | | -2. 业务对象汇总一组根据对象名确定的模型(每个模型对应一个资源文件)。 |
45 | | -3. 一组业务对象构成一个模块,特别是它们的ORM模型是一个整体。 |
| 173 | +### ORM优化 |
46 | 174 |
|
47 | | -访问一个模块中的任何资源时都会导致先初始化模块。这个初始化过程就是自动生成一组基本的资源文件。 |
| 175 | +- **内存批量处理**:利用NopORM的batchLoad机制 |
| 176 | +- **懒加载**:按需加载关联实体 |
| 177 | +- **模块级优化**:ORM模型按模块整体加载 |
48 | 178 |
|
49 | | -所有的DynamicXXXProvider都可以选择提供租户支持,根据上下文中的租户id来动态确定如何加载。并且内部会持有租户特定的缓存。 |
| 179 | +## 其他 |
| 180 | +ConfigProvider目前考虑不进行动态化。首先,动态配置已经通过Nacos配置中心支持。第二,每个租户如果有特殊配置应该使用另外一个配置获取接口,而不是使用全局的ConfigProvider。 |
| 181 | +全局的ConfigProvider在所有模型加载的时候都会被读取,如果混合使用,有可能全局模型动态加载的时候读取到了租户的配置。 |
50 | 182 |
|
51 | | -整体设计模式是Loader as Generator |
| 183 | +## 设计理念总结 |
52 | 184 |
|
53 | | - Model = Loader(virtualPath X tenantId) |
| 185 | +1. **关注点分离**:各Provider职责单一,边界清晰 |
| 186 | +2. **租户优先**:原生支持多租户隔离和资源管理 |
| 187 | +3. **按需加载**:Lazy加载策略,最小化资源占用 |
| 188 | +4. **无状态设计**:简化并发控制和分布式部署 |
| 189 | +5. **可扩展架构**:通过标准接口支持定制化实现 |
54 | 190 |
|
55 | | -在加载器中动态确定如何加载模型。 |
| 191 | +该架构在保持系统简单性的同时,为动态模型管理和多租户支持提供了完整的技术基础,特别适合需要高度灵活性和租户隔离的企业级应用场景。 |
0 commit comments