Skip to content

Commit ea05650

Browse files
committed
docs: synced via GitHub Actions
1 parent 015768a commit ea05650

11 files changed

+3666
-46
lines changed
Lines changed: 169 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,191 @@
1-
# 动态模型设计
1+
以下你觉得怎么样
2+
# Nop平台动态模型加载与多租户支持架构设计
23

3-
## 整体设计
4+
## 设计概览
45

5-
整个设计思想如下:
6+
Nop平台采用统一的动态模型加载机制,支持多租户环境下的资源隔离。核心设计基于"Loader as Generator"模式:`Model = Loader(virtualPath × tenantId)`,通过标准化的Provider接口实现模型的按需加载和缓存管理。
67

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+
// 统一业务访问接口
826
IBizObject bizObj = bizObjectManager.getBizObject(bizObjName);
927
bizObj.invoke(actionName, request, selection, svcCtxt);
1028
```
1129

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+
### 并发安全
18144

19-
最小的动态加载粒度是单个IResource资源文件。IDynamicResourceProvider加载的时候会根据虚拟文件路径动态决定如何生成一个InMemoryTextResource。
145+
- **无状态模型**:纯逻辑结构,无状态迁移问题
146+
- **线程安全缓存**:LoadingCache + Double Check锁定
147+
- **分布式同步**:消息总线通知缓存失效
20148

21-
对于动态生成的BizObject,它实际上对应于两个资源文件,XMeta和XBiz。IDynamicBizModelProvider本质上只是根据对象名来动态确定两个路径。
149+
## 错误处理策略
22150

23-
每个对象属于一个模块Module。访问该模块的任何资源文件会导致自动执行该模块的动态初始化代码。这可能导致预先生成一批InMemoryTextResource。原先加载一批代码等。
24-
比如加载模块的orm-interceptor监听器。
151+
### 异常处理原则
25152

26-
有些资源的实际加载粒度是模块。比如OrmModel。按照实体名去动态加载实体模型时,会动态确定它是哪个模块的。然后一次性加载该模块的OrmModel,然后把它合并到当前的OrmModel中。
153+
- **快速失败**:资源未找到或加载失败立即抛出异常
154+
- **统一异常**:遵循NopException规范
155+
- **可扩展降级**:通过替换Loader实现定制策略
27156

28-
所有加载都是Lazy的,也就是说如果访问了一个模块的BizObject,但是如果没有用到该模块的ORM,则实际不会去加载这个模块的实体模型。
157+
### 典型错误场景
29158

30-
动态加载器总是最后使用。也就是说如果在缓存中或者当前虚拟文件系统中已经存在,则返回已经存在的资源。
159+
| 场景 | 处理方式 | 恢复策略 |
160+
|------|----------|----------|
161+
| 模型文件未找到 | 抛出NopException | 检查路径配置 |
162+
| 加载过程异常 | 直接抛出异常 | 修复底层问题 |
163+
| 租户资源隔离 | 租户特定异常 | 检查租户权限 |
31164

32-
核心逻辑如下:
33-
1. virtualPath => DynamicGeneratedResource
34-
2. bizObjName => virtualPath
35-
3. entityName => moduleName => DynamicGeneratedOrmModel
36-
4. virtualPath => moduleName => Init Module Once
165+
## 性能优化
37166

38-
IDynamicBizModelProvider.getBizObjNames() 提供所有BizObjName名称,包含所有未加载的业务对象的名称
167+
### 缓存策略
39168

40-
IDynamicModuleDiscovery.getEnabledModules() 返回所有可用的模块,但是实际访问之前并没有加载。
169+
- **多级缓存**:租户隔离 + 共享缓存
170+
- **智能回收**:基于LRU的自动内存管理
171+
- **分布式同步**:跨节点缓存一致性
41172

42-
也就是说存在三种粒度:
43-
1. 单个资源文件(但是解析或者生成资源文件的过程中可能会引入更多的资源文件依赖)
44-
2. 业务对象汇总一组根据对象名确定的模型(每个模型对应一个资源文件)。
45-
3. 一组业务对象构成一个模块,特别是它们的ORM模型是一个整体。
173+
### ORM优化
46174

47-
访问一个模块中的任何资源时都会导致先初始化模块。这个初始化过程就是自动生成一组基本的资源文件。
175+
- **内存批量处理**:利用NopORM的batchLoad机制
176+
- **懒加载**:按需加载关联实体
177+
- **模块级优化**:ORM模型按模块整体加载
48178

49-
所有的DynamicXXXProvider都可以选择提供租户支持,根据上下文中的租户id来动态确定如何加载。并且内部会持有租户特定的缓存。
179+
## 其他
180+
ConfigProvider目前考虑不进行动态化。首先,动态配置已经通过Nacos配置中心支持。第二,每个租户如果有特殊配置应该使用另外一个配置获取接口,而不是使用全局的ConfigProvider。
181+
全局的ConfigProvider在所有模型加载的时候都会被读取,如果混合使用,有可能全局模型动态加载的时候读取到了租户的配置。
50182

51-
整体设计模式是Loader as Generator
183+
## 设计理念总结
52184

53-
Model = Loader(virtualPath X tenantId)
185+
1. **关注点分离**:各Provider职责单一,边界清晰
186+
2. **租户优先**:原生支持多租户隔离和资源管理
187+
3. **按需加载**:Lazy加载策略,最小化资源占用
188+
4. **无状态设计**:简化并发控制和分布式部署
189+
5. **可扩展架构**:通过标准接口支持定制化实现
54190

55-
在加载器中动态确定如何加载模型
191+
该架构在保持系统简单性的同时,为动态模型管理和多租户支持提供了完整的技术基础,特别适合需要高度灵活性和租户隔离的企业级应用场景

src/theory/ddd-rethinking-transaction-boundaries.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 领域驱动设计(DDD)中聚合根的最主要职责真的是维护一致性吗?
22

3-
> 在上一篇文章[《领域驱动设计(DDD)领域对象一定要讲究充血模型吗?》](https://mp.weixin.qq.com/s/IrG5arGQj-fB1i4W_5ZFKA)中,我已阐明:聚合根的深层价值在于**领域信息的表达**,而非单纯的一致性守护。但是如果询问GTP等AI大模型,会发现它们总是坚守DDD社区的主流观点,将聚合根的核心职责固化为“维护一致性”。
3+
> 在上一篇文章[《领域驱动设计(DDD)领域对象一定要讲究充血模型吗?》](https://mp.weixin.qq.com/s/IrG5arGQj-fB1i4W_5ZFKA)中,我已阐明:聚合根的深层价值在于**领域信息的表达**,而非单纯的一致性守护。但是如果询问GPT等AI大模型,会发现它们总是坚守DDD社区的主流观点,将聚合根的核心职责固化为“维护一致性”。
44
55
> 从可逆计算理论的视角审视,这一认知亟待纠偏。本文将基于此理论,深入揭示一种更具演化能力的全新架构范式。
66
@@ -93,42 +93,42 @@ public class Order {
9393
### 核心概念模型
9494

9595
1. **数据聚合 (Data Aggregate)**
96-
96+
9797
* **职责**:成为领域语言最廉价、最直观的载体,构建一张统一的“信息访问地图”。它封装了底层数据实体,并提供了加载关联对象的能力。
9898
* **特征**:仅承载领域结构性数据与最小核心结构不变式(例如:金额不能为负)。它不主动参与任何策略决策,但通过`Manager``Cache`机制,高效地为上层逻辑提供完整的、富含业务语义的信息视图。`order.getCustomer().getCreditLimit()` 这样的代码本身就是一句清晰的领域表达。
9999

100100
2. **行为聚合 (Behavior Aggregate / Orchestrator)**
101-
101+
102102
* **职责**:通过流程编排器,将原先聚合根的大方法拆分为一条有序的“步骤链”。
103103
* **特征**:不直接承载领域状态,而是通过编排多个`步骤(Step)`来完成业务操作。它可以被动态组合和扩展,以适应不同的业务需求(如租户定制)。
104104

105105
3. **流程步骤 (Step)**
106-
106+
107107
* **职责**:保证一个局部不变式或执行一个单一的业务变换。
108108
* **特征**:输入为共享的`上下文(Context)`,输出是对聚合数据的局部变换或校验结果。每个`Step`职责单一,易于单元测试和替换。
109109

110110
4. **Kit 接口**
111-
111+
112112
* **职责**:抽象并封装外部可变的能力,如定价策略、库存检查、风险评分等。
113113
* **特征**:通过依赖倒置,将易变的策略从稳定的流程中解耦出去,支持通过表达式、规则引擎或远程服务等多种方式实现。
114114

115115
5. **上下文 (Context / Blackboard)**
116-
116+
117117
* **职责**:在流程执行期间,作为共享的数据载体,传递数据聚合实例、IDataCache引用以及其他辅助信息。
118118
* **特征**:避免了方法参数爆炸,并允许在不破坏接口签名的情况下轻松扩展流程所需的信息(如tracingId、幂等键)。
119119

120120
这种**“结构(数据)”与“动力学(流程)”的彻底分离**,是新范式成功的关键。
121121

122122
> 这种做法类似于函数式编程中的 **ADT(代数数据类型)+ 纯函数** 模式:
123-
>
123+
>
124124
> - **数据聚合 ≈ ADT**:一个纯粹的、透明的数据结构
125125
> - **业务步骤 ≈ 纯函数**:一个个接收数据、执行单一职责、内部无状态的逻辑单元
126126
127127
### After: 重构后的"下单"流程
128128

129129
以下示例精准地模拟了Nop平台的设计哲学:与Spring深度集成,通过外部化配置实现声明式、可演化的业务流程。
130130

131-
> 如果直接使用Nop平台会更加简单,可以通过元编程减少更多胶水代码。在不使用Nop平台的情况下,5000行代码大概可以实现一个精简版本
131+
> 如果直接使用Nop平台会更加简单,可以通过元编程减少更多胶水代码。在完全从零开始编写的情况下,5000行代码大概可以实现一个模拟Nop平台的精简框架
132132
133133
![Nop DDD Archecture](ddd/nop-ddd-arch.svg)
134134

@@ -325,7 +325,7 @@ CQRS(Command Query Responsibility Segregation)将系统明确划分为命令端
325325
- **性能问题的解决之道:统一的接口,差异化的实现**
326326
通过 `order.getCustomer().getAddress()` 进行深层次导航是表达领域逻辑的理想方式,但其传统的ORM实现常导致N+1查询问题。
327327
本文所倡导的 `Manager``IDataCache` 机制,结合**批量加载器**,正是为此而生。更重要的是,**因为业务代码依赖的是"数据聚合"的接口,而非具体实现**,我们可以在查询端注入一个完全不同的、高度优化的实现。
328-
328+
329329
- **批量加载**:在一个请求周期内,框架可以拦截所有对关联数据的访问,将其合并为一次高效的批量查询。
330330
- **实现切换**:以Nop平台为例,其ORM底层可以灵活切换`Driver`实现。对于复杂查询,可以切换到使用定制SQL或NoSQL的驱动,通过一次查询完成所有数据的加载与组装,从而在保持上层领域代码纯净的同时,彻底解决性能瓶颈。
331331

@@ -353,17 +353,17 @@ CQRS的提出,非但没有否定DDD的价值,反而通过**职责的物理
353353

354354
1. **统一的REST API入口**
355355
平台提供一个标准的RESTful API入口,其格式为 `/r/{bizObjName}__{bizAction}`。这个URL结构本身就是一种强有力的“可发现”机制。
356-
356+
357357
* `{bizObjName}`:业务对象的唯一标识名,如 `CmsArticle``OmsOrder` 等。
358358
* `{bizAction}`:在该业务对象上执行的具体动作名,如 `publish``cancel``refund` 等。
359359

360360
2. **动态的行为组装与分派 (Dynamic Behavior Assembly & Dispatch)**
361361
框架并非通过一个庞大的`switch`来分派逻辑。恰恰相反,它在运行时或启动时**动态地组装**一个业务对象的全部行为:
362-
362+
363363
* **行为切片 (Behavior Slices)**:框架会自动扫描所有与`OmsOrder`相关的“行为切片”。这些切片可以来自基础平台、行业解决方案包、甚至特定租户的定制模块,形式可以是Java类(`BizModel`)或XDSL模型文件(`.xbiz`)。
364-
364+
365365
* **叠加与覆盖 (Overlay & Override)**:框架将收集到的所有行为切片,按照预设的**优先级**进行“叠加”。这就像一个为业务逻辑设计的“**PhotoShop图层**”:高优先级的定义(如租户定制的`refund`行为)会自动覆盖低优先级的同名行为(如平台基础的`refund`行为),而`XBiz`模型文件中的定义拥有最高优先级。
366-
366+
367367
* **生成动态分派器**:叠加完成后,框架会在内存中为每个`IBizObject`生成一个高效的**行为分派映射表**(例如`Map<String, IBizAction>`)。当`invoke`方法调用时,仅需一次 O(1) 复杂度的查表即可执行对应的逻辑(如一个`TaskFlow`流程)。
368368

369369
**这个模型的颠覆性优势在于:**

0 commit comments

Comments
 (0)