业务扩展框架03:组合实验与远程绑定
运行一个最小业务组合实验
在博客源码仓库根目录运行:
1 | |
示例仅依赖 JDK 8 或更新 JDK,不下载第三方依赖。脚本在临时目录编译,运行后清理编译产物。完整代码位于仓库的 examples/business-extension/ExtensionLab.java,实验说明位于同目录 README.md。
这是自建教学模型,不是 TMF、Lattice 或美团 BPF 的源码。它验证 SPI 发现、注解注册、业务配置激活和本地扩展调用。库存、资格与销售时间仅记录 mock 事件,没有连接真实系统。
在 Java 8 下执行,三个业务输出如下:
1 | |
三次输入的原始金额都是 10000 分。九折、八折是人为规定的教学规则,不代表真实平台定价,也没有模拟订金、尾款或成团状态机。
最后一行来自独立的 TypedExtensionLab.java。它验证带类型句柄、输入输出检查与伪造契约拒绝。该文件包含故意绕过泛型检查的 raw 类型反例,编译时会显示 unchecked 警告;运行时预期在输出检查处拒绝错误类型。普通注册与调用代码不使用未经检查的强转。
相同流程怎样得到三种行为
产品关系用 Java Map 表达:
1 | |
这里的产品是教学上的组合单元。三个业务都复用基础校验和审计;预售与团购分别增加自己的校验、价格和额度动作。所有实现都已经加载进 JVM,但只有被当前业务关联的规则能成为候选。
实现类的注解只声明身份和所属调用点:
1 | |
90 是实现代码中的常量,改变它需要修改代码。这个实验没有把折扣率也做成动态配置。可配置的是实现选择、优先级和最低适用金额;不能看到配置表就宣称所有业务规则都能在线修改。
执行引擎也没有判断“这是预售,所以调用预售价格”。它只运行固定的三个阶段:
1 | |
候选收集读取注解和配置,而不枚举业务名称:
1 | |
rules 已经由业务身份对应的产品关系算出。原价规则与预售价规则都适用时,预售价优先级 10 高于原价的 0,SINGLE 只执行预售价。库存阶段的 ALL 则先执行优先级 100 的审计,再执行业务额度动作。
图 1:以 10000 分输入对照三个业务;阶段顺序相同,配置与候选选择决定实际动作。
flowchart TB
accTitle: 三个业务共用流程与不同候选选择
Engine["同一执行引擎:VALIDATE → PRICE → INVENTORY"]
subgraph Retail["retail:base + retail"]
direction TB
RV["VALIDATE<br/>基础校验"] --> RP["PRICE<br/>仅原价候选"]
RP --> RI["INVENTORY<br/>审计 → 普通库存 mock"]
RI --> RO["应付 10000 分"]
end
subgraph Presale["presale:base + presale"]
direction TB
PV["VALIDATE<br/>基础 → 预售时间 mock"] --> PP["PRICE<br/>原价 / 预售价<br/>选择高优先级预售价"]
PP --> PI["INVENTORY<br/>审计 → 预售额度 mock"]
PI --> PO["应付 9000 分"]
end
subgraph Group["group:base + group"]
direction TB
GV["VALIDATE<br/>基础 → 团购资格 mock"] --> GP["PRICE<br/>原价 / 团购价<br/>选择高优先级团购价"]
GP --> GI["INVENTORY<br/>审计 → 团购额度 mock"]
GI --> GO["应付 8000 分"]
end
Engine --> RV
Engine --> PV
Engine --> GV
这证明了配置可以改变现有实现的组合。它没有证明平台能够自动发现产品,也没有证明相同接口下的任意实现可以安全组合。
接入一个复用现有能力的新业务
若新业务 community 恰好完全沿用既有团购规则,只需在样例的业务关系中增加:
1 | |
通过发布校验后,运行 engine.execute("community", 10000) 会选择团购价与团购额度。无需修改 Engine.invoke(),无需增加 CommunityPrice。完整程序包含该组合的断言,用于检验这项复用结论。
图 2:教学示意:新增 community 业务只增加组合关系,复用既有团购实现。
flowchart TB
accTitle: 教学示意:community 复用既有团购实现
Change["本次配置差异<br/>增加 community → base、group"] --> Publish["构建版本 5 快照并通过 publish 校验"]
Publish --> Request["execute:community,10000 分"]
Request --> Expand["Snapshot.rules 展开产品关系<br/>按能力 ID 去重"]
Base["复用 base<br/>input、list-price、audit"] --> Expand
Group["复用 group<br/>group-check、group-price、group-quota"] --> Expand
Expand --> Run["既有 Engine 执行三个阶段"]
Run --> Result["应付 8000 分<br/>事件列表与 group 业务相同"]
Scope["样例仍需重新编译<br/>未实现配置中心或在线热发布"] -.-> Change
这里配置仍然写在 Java 样例里,所以修改 Map 后需要重新编译实验程序;“无需修改执行引擎”不等于已经实现线上无重启发布。生产实现可以从 JSON 或配置中心构建相同的候选快照,这需要额外的配置读取与发布设施。
如果新业务还要求“成团后才预留库存”,则现有三阶段执行顺序可能不足。此时需要增加状态或修改流程契约,不能只把 group 和 presale 产品都安装上。教学代码的 SINGLE 冲突保护恰好会拒绝两个同优先级价格同时适用的情形。
成功输出以外还需要验证什么
实验主程序的断言按行为组织:
| 场景 | 预期结果 | 要防止的错误 |
|---|---|---|
| 未知业务或不存在的配置引用 | 拒绝请求或拒绝发布 | 拼错配置却使用默认逻辑 |
| SINGLE 最高优先级并列 | 拒绝执行 | 依赖类加载或 Map 顺序碰巧选中 |
| 没有适用计价候选 | 使用唯一默认价 | 缺省行为含混 |
| 已选计价实现抛异常 | 异常继续传播 | 依赖故障被伪装成正常原价 |
| 非法发布与合法新版本 | 非法版本不替换;请求使用一次取得的快照 | 请求混用不同版本的产品和规则 |
金额校验额外覆盖负数、按分舍入和大整数。计算折扣使用 BigDecimal,最终转换为 long 分;避免先用 long 乘百分比造成中间溢出。
图 3:教学示意:PRICE 无候选、候选冲突与实现故障走不同分支。
flowchart TB
accTitle: 教学示意:PRICE 的默认、冲突与故障分支
Filter["筛选已激活的 PRICE 规则<br/>原始金额达到 minCents"] --> Sort["优先级降序<br/>同优先级按 implementation 排序"]
Sort --> Empty{"候选为空?"}
Empty -->|是| Default["执行注册表中的 default-price<br/>保留原价,记录 price.default"]
Empty -->|否| Tie{"最高优先级并列?"}
Tie -->|是| Reject["抛出 ambiguous SINGLE<br/>本次请求终止"]
Tie -->|否| Selected["只执行最高优先级候选"]
Selected --> Outcome{"实现正常返回?"}
Outcome -->|否| Error["异常继续传播<br/>不改用 default-price"]
Outcome -->|是| Next["校验应付金额非负<br/>通过后进入 INVENTORY"]
Default --> Next
实验的快照测试证明已有请求对象持有固定引用,以及非法发布后旧快照仍可用。它没有做多进程一致性或并发压力测试。两个应用各自更新到不同版本的问题,不能靠单 JVM 的 AtomicReference 解决。
ALL 顺序执行也不意味着事务。如果第二个库存动作抛异常,第一个动作的副作用可能已经发生。当前 mock 只记录事件,没有真实扣减,因此不会产生库存损失;生产系统需要独立设计幂等、持久化状态与补偿。
图 4:教学示意:库存阶段只追加内存事件,未验证真实库存事务。
sequenceDiagram
title 教学示意:库存 mock 的执行与验证边界
participant Engine as Engine.invoke
participant Audit as Audit
participant Quota as PresaleQuota
participant Events as Context.events
Note over Engine,Events: 预售业务的 INVENTORY 阶段,全部在单 JVM 内执行
Engine->>Audit: apply(context),优先级 100
Audit->>Events: 追加 inventory.audit
Audit-->>Engine: 正常返回
Engine->>Quota: apply(context),优先级 10
Quota->>Events: 追加 inventory.presale-quota(mock)
Quota-->>Engine: 正常返回
Note over Quota,Events: 未连接库存服务或数据库<br/>未执行真实库存扣减与事务提交
Note over Engine,Events: 事件顺序可以断言<br/>真实扣减的幂等与补偿仍需单独实现和验证
接口与实现究竟有没有远端绑定
需要分别回答两个问题:
1 | |
注册表可以返回直接执行本地逻辑的实现,也可以返回封装远程调用的客户端或代理。是否跨进程,取决于调用链是否发送网络请求;代理本身也可以只做日志、鉴权或本地路由。
JDK 动态代理把接口方法调用交给 InvocationHandler;处理器可以直接调用本地对象,也可以编码请求并交给 RPC 客户端。JDK Proxy 文档。本实验直接调用本地 Action,没有伪装成已经实现 RPC。
Lattice:有明确的远程扩展机制
Lattice 的 2022-10-01 教程展示了 protocolType = ProtocolType.REMOTE。调用方使用远程客户端与 Dubbo runner,另一进程运行插件容器,业务实现部署在那里。教程提供了使用 Dubbo、Nacos 的样例,而不是 Java RMI。Lattice:RPC 调用扩展点、远程示例仓库。
该版本文档还限制 REMOTE 的业务叠加,并建议必要时在远端容器内部完成本地叠加。这是有意限制远程调用规模的设计,不能把本地多实现归约无条件搬到网络上。
图 5:Lattice 文档中的远程扩展角色与调用方向。
sequenceDiagram
title Lattice 文档中的远程扩展角色与调用方向
participant App as 业务主服务
participant Client as Lattice远程客户端
participant RPC as Dubbo调用层
participant Container as 远端插件容器
participant Impl as 业务扩展实现
App->>Client: 扩展点调用与业务上下文
Client->>RPC: 使用远程runner发起调用
RPC->>Container: 网络请求
Container->>Impl: 定位并执行业务实现
Impl-->>Container: 返回结果
Container-->>RPC: 响应
RPC-->>Client: 调用结果
Client-->>App: 扩展点结果
图表达公开文档中的职责关系,未声称列出了所有内部类与报文字段。本次未部署该分布式示例,也不能把它证明的功能直接归给阿里内部 TMF2 的当前版本。
OCTO:从接口调用进入服务治理与通信链
OCTO 是美团公开的微服务通信与治理体系。它的 Appkey、服务实例发现、路由与负载均衡解决远端服务定位问题;这些对象并不天然包含“预售产品安装哪些业务能力”。美团 OCTO 开源介绍,2019-08-08。
公开 Dorado 源码可以确认一条具体链路。在提交 b7aaefe900368cfc4edf1fd793a2521e9f48aec1 中,JdkProxyFactory 调用 Proxy.newProxyInstance,把接口代理绑定到 DefaultInvocationHandler;后者将方法与参数封装为 RpcInvocation,交给 handler 处理。
接口在调用方得到的是代理,真正的业务处理在远端进程完成。代理提供相似的调用语法,却不能消除网络失败、超时和重复请求。
图 6:OCTO 代理入口依据固定提交源码,后续通信与服务端仅表示职责角色。
sequenceDiagram
title OCTO 从 Java 接口代理到远端服务的职责时序
participant App as 调用方
participant Proxy as JDK接口代理
participant Handler as DefaultInvocationHandler
participant Chain as 调用与通信链(角色)
participant Server as 远端服务(角色)
App->>Proxy: 调用接口方法(args)
Proxy->>Handler: invoke(proxy, method, args)
Handler->>Handler: 将接口、Method与参数封装为RpcInvocation
Handler->>Chain: handler.handle(invocation)
Chain->>Chain: 实例选择与请求编码
Chain->>Server: 经网络发送请求
Server->>Server: 分派并执行服务方法
Server-->>Chain: 网络响应
Chain-->>Handler: RpcResult
Handler-->>Proxy: result.getReturnVal()
Proxy-->>App: 返回业务结果
Dorado 还把代理工厂等基础设施定义为 SPI 扩展。这与“根据业务身份挑选预售价”处于不同层次:前者决定框架如何生成代理或如何通信,后者决定业务采用什么规则。ProxyFactory.java、OCTO-Dorado 官方 Wiki。
华为云 CSE 与 ServiceComb:服务引用注解的对照
华为云 CSE 的公开定位包括注册发现、配置管理和服务治理,并支持 ServiceComb 等生态。这里选用 Apache ServiceComb 的公开文档作对照,不推断华为内部存在与 TMF 相同的业务产品模型。华为云:什么是微服务引擎。
ServiceComb 的跨应用调用文档给出了服务引用声明。其关键形态是:
1 | |
这段历史 API 示例指向一个应用下的微服务与 schema。它说明注解可以声明“需要哪个远程服务契约”,而实际实例地址由框架与服务发现处理。具体集成应匹配所使用的 ServiceComb 版本。ServiceComb:Cross-application invocation。
该跨应用示例还要求服务提供方设置 service_description.properties.allowCrossApp: true。仅添加字段上的引用注解,并不足以完成跨应用调用。
它与 @Extension(id="presale-price", point=PRICE) 的建模对象不同:一个描述服务引用,一个描述本地业务扩展实现。业务组件可以依赖远程引用,但不能因此把远程引用本身解释为领域能力组合。
与RMI相似的部分,及其失效的地方
RMI 与这些 RPC 方案都可以让调用方通过接口或代理发起调用,参数被传到另一个进程执行。相似性停留在远程调用抽象;Java RMI 有自己的远程对象与调用体系,OCTO 使用其通信协议和治理机制,Lattice 示例接入 Dubbo。不能因为“像调用本地接口”就把它们称为 RMI。Java RMI 系统概述。
把业务组件远程化后,至少增加三个必须明确的契约:
- 调用结果未知时如何处理。客户端超时可能发生在远端完成写入之后,重试需要幂等键或可查询的执行记录。
- 上下文如何跨进程传递。业务身份、租户、trace 和配置版本的语义需要双方约定;传一个同名字段不等于双方规则一致。
- 失败能否降级。资格校验或价格计算失败后使用默认实现,可能改变商业结果,必须由业务契约明确授权。
图 7:响应丢失后的同键重试教学设计;业务幂等由应用实现,不表示 OCTO 提供此保证。
sequenceDiagram
title 远端写入成功但响应丢失后的幂等重试
participant Client as 业务客户端
participant Service as 库存服务
participant Store as 持久化存储
Client->>Service: 预留库存(幂等键K,请求内容P)
Service->>Store: 事务:登记K与P、扣减库存、保存结果R
Note right of Store: K具有唯一约束<br/>业务写入与幂等记录<br/>处于同一事务
Store-->>Service: 提交成功,返回R
Service--xClient: 成功响应在网络中丢失
Client->>Client: 等待超时,结果未知
Client->>Service: 使用相同K与P重试
Service->>Store: 查找K对应的请求与结果
Store-->>Service: 已完成:P与R
Service->>Service: 核对请求内容一致,不再扣减
Service-->>Client: 返回已保存的结果R
网络开销也会影响扩展颗粒度。一个领域能力内部十个细小扩展点若各自跨进程,调用数量、失败概率和版本兼容面都会扩大。将具有一致事务或数据需求的一组规则放在同一个远程边界内,通常更容易管理;最终仍需要实际延迟与失败预算验证。
图 8:教学设计:跨进程传递配置版本,还需约定对应规则的含义。
flowchart TB
accTitle: 教学设计:跨进程配置版本的语义约定
subgraph Local["已实现的本地实验"]
Read["execute 读取一次 published"] --> Ref["Context 持有 Snapshot 引用"]
Ref --> Rules["三个阶段使用该快照展开的规则"]
end
subgraph Design["远程化时需要补充的契约:实验未实现"]
Context["请求上下文<br/>业务身份、租户、trace、配置版本"] --> Transport["跨进程传递字段"]
Transport --> Meaning{"双方是否约定版本<br/>对应的规则内容?"}
Meaning -->|已约定| Check["按约定选择规则<br/>并验证兼容性与失败语义"]
Meaning -->|未约定| Gap["同名字段不足以证明<br/>双方执行相同规则"]
end
Ref -.->|扩展到跨进程时| Context
Boundary["本地 AtomicReference<br/>只更新本 JVM 的引用"] -.-> Ref
一条可实施的组合链路
图 5 是基于前述机制推导的架构,不代表已证实的 BPF 内部实现:
图 9:业务规则选择与远程服务定位分层的教学架构。
flowchart TB
accTitle: 业务规则选择与远程服务定位分层的教学架构
Request["可信业务身份与请求事实"] --> Config["业务配置版本"]
Config --> Product["激活产品与能力"]
Product --> Policy["选择业务扩展实现"]
Policy --> Local["本地计算"]
Policy --> Adapter["远程服务适配器"]
Adapter --> Proxy["RPC接口代理"]
Proxy --> Discover["服务发现、路由、实例选择"]
Discover --> Remote["远端领域服务"]
适配器对上保持领域契约,对下使用 OCTO、ServiceComb 或其他 RPC 客户端。业务身份在上层选择规则,服务标识在下层选择服务,实例选择再决定某台机器。三个决策可以关联,但不宜复用同一套命名和生命周期。
平台能力复用到这里仍有可观察的判据:相同实现是否被多个业务调用,路由是否能解释,新增配置是否通过契约测试,失败时是否保持预期语义。注解是建立目录的一种方式;是否能快速接入业务,要由这些行为证明。
实验范围
本次运行证明了单进程教学模型的组合、选择、默认与错误处理。Lattice 远程扩展依据公开教程,OCTO 代理链依据固定提交源码,ServiceComb 依据公开文档;未运行它们的跨进程环境。
美团 BPF 的正式定义与实现仍未由本次检索确认。这里提供的是可追溯的公开机制与可运行推导,不以推导填补厂商事实。




