业务扩展框架02:注解模型与表驱动
注解把组件看成什么
业务框架读取一个类上的注解时,通常是在构建组件描述信息:这个实现的稳定标识是什么、实现哪个扩展点、归属哪种业务或产品、遵守什么调用契约。Java 对象负责执行代码,描述信息负责让框架管理和选择这个对象。
“被注解的组件”可能对应不同对象:
| 注解所在位置 | 可能建模的对象 | 不能仅凭注解得出的结论 |
|---|---|---|
| 领域接口或抽象类 | 能力定义、服务契约、流程节点类型 | 已经存在可执行实例 |
| 扩展接口的方法 | 一个变化点及其输入输出、归约语义 | 有多少业务实现会被激活 |
| 具体实现类 | 某个能力实现及其归属 | 本次请求一定执行它 |
| 配置加载器、代理工厂 | 框架本身的可替换基础设施 | 它属于某个商业产品 |
| 服务引用字段 | 远程服务引用的声明 | 远端实例已经发现且可用 |
因此同样叫 @Extension,在不同框架里可能标记扩展点定义,也可能标记扩展实现。理解注解必须同时看 @Target、处理器和注册过程;仅凭名字无法确定语义。
图 1:教学对照:注解所在位置与处理器共同决定被登记的对象;这些分支不代表同一个框架的统一 API。
flowchart TB
accTitle: 注解位置与处理器如何决定组件模型
Source["带注解的声明"] --> Where{"注解标在哪里?"}
Where -->|接口方法| Point["扩展点定义"]
Where -->|具体实现类| Impl["扩展实现描述"]
Where -->|引用字段| Ref["服务引用声明"]
Point --> P["登记点ID、签名、归约约定"]
Impl --> I["登记实现ID、所属点、实例"]
Ref --> R["由容器解析引用并注入代理"]
P --> Reader["对应处理器解释这些元数据"]
I --> Reader
R --> Reader
Reader --> Separate["业务配置另行决定本次采用谁"]
第一篇 描述了业务模型。这里用 Lattice、Camp 的公开声明与一份独立教学实现,拆解注解、注册表、配置表和请求上下文的关系。
真实框架的注解并不是同一种模型
Lattice:定义变化点,再声明实现归属
Lattice 的 2022 年 Quickstart 在扩展接口的方法上使用 @Extension,示例包含 code、name、reduceType 三个显式属性;另由 @Business 声明业务,@Realization 关联实现归属,能力对象负责发起扩展调用。Lattice Quickstart。
这里的三个属性是那段示例显式写出的属性数,不是该注解类全部字段数。不能把一个使用片段当成完整 API 定义。后续远程调用示例还显式设置 protocolType,版本和使用场景会影响实际需要哪些字段。
其建模可以归纳为:
1 | |
第一行定义“允许定制什么”,第二行登记“谁提供定制”,第三行管理组合关系。把“属于某产品”与“被某业务安装”分开,同一个产品才不必为每个业务复制一次代码。
Camp:让 Action 成为可管理的能力节点
美团 Camp 的公开样例在抽象 Action 类上标记 @LppAbility,在具体子类上标记 @LppExtension。前者示例显式写了七个属性:name、desc、param、response、prd、func、cost;后者显式写了五个属性:name、func、diff、prd、cost。美团 Camp §4.3.2。
这些字段既包含输入输出说明,也包含需求和差异描述。它们帮助构建可查询的能力目录。公开片段不足以确认 cost 的精确单位和调度语义,不能擅自解释成“超时毫秒数”或“线程优先级”。
Camp 的编译阶段元数据采集、后续配置与运行调度表明:注解可以参与管理工具建设,不只是帮助 Spring 创建 Bean。至于输入输出依赖如何分析,还需要额外的静态分析;注解文字本身不会证明节点可并行。
静态与动态应分成三个层次
Java 注解中的元素值有语言限制。常见的基本类型、字符串、枚举、类字面量、注解与允许类型的数组可以出现;任意对象、运行时方法调用不能直接成为注解参数。RetentionPolicy.RUNTIME 的含义是元数据保留到运行期供读取,不是属性在每次请求时自动重新计算。JLS 9.6.1、9.7.1。
例如,下面是假想注解使用方式,仅解释语言边界:
1 | |
即使注解里写了 "${pricing.policy}",它在 Java 层面也只是一段固定字符串。只有框架显式实现占位符解析、读取配置和刷新逻辑,才会获得动态意义。静态的是配置键,动态的是键对应的值。
三层模型比“注解一半静态、一半动态”准确:
| 层次 | 示例 | 何时变化 | 谁负责解释 |
|---|---|---|---|
| 代码元数据 | 实现 ID、扩展点 ID、默认实现标志、方法签名 | 构建或代码版本变化 | 注册器、注解处理器、容器 |
| 发布配置 | 产品安装关系、优先级、匹配阈值、流程版本 | 配置发布时 | 配置校验器与运行时快照 |
| 请求事实 | 业务身份、金额、用户选项、库存查询结果 | 每次请求或业务步骤 | 业务入口与领域逻辑 |
图 2:静态目录、发布配置与请求事实在候选选择处汇合。
flowchart TB
accTitle: 静态目录、发布配置与请求事实在候选选择处汇合
subgraph Code["代码版本"]
Ann["注解:id / point / fallback"] --> Catalog["实现目录"]
Signature["接口方法:输入与输出类型"] --> Catalog
end
subgraph Release["配置版本"]
Product["业务安装哪些产品"] --> Rule["实现选择 / 优先级 / 金额阈值"]
end
subgraph Request["单次请求"]
Facts["可信业务身份 / 原始金额"]
end
Catalog --> Pick["按契约收集适用候选"]
Rule --> Pick
Facts --> Pick
Pick --> Execute["排序、处理冲突、执行"]
有些字段放在哪层是设计取舍。归约策略可以固定在扩展点契约中,也可以允许配置,但必须限制兼容范围。例如返回 boolean 的校验扩展可以定义“全部通过”,返回金额的扩展不能随意切换成布尔聚合。
业务 ID 同样不是天然静态或动态:实现上声明支持某业务,是静态归属;请求携带某业务,是动态事实;允许该业务启用某产品,是发布配置。三者即使用了相同字符串,角色仍然不同。
一份能数清字段的教学注解
配套实验的注解有且只有三个自定义元素:
1 | |
这是本系列自建的 ExtensionLab.Extension,不是 Lattice 或美团的注解。它把具体类视为“可注册的扩展实现”。
id 标识实现,注册器用它检查重复并解析配置引用。point 指明实现属于校验、计价还是库存阶段。fallback 表示它能否作为对应扩展点的缺省实现。三个元素均为代码静态元数据,直接承载的请求动态值是零个。
方法签名从 Action.apply(Context) 取得,不在注解里重复声明。实例由插件提供者构造,注册器没有把类的每个字段都转成配置。为了缩小实验,它也没有 owner、版本范围、依赖图等生产目录字段;这些信息可能有用,但不是实现路由必需的条件。
在这个模型里,配置层另有三组关系和三个规则属性:
1 | |
整个快照还带一个 version。这里的 capabilityIds 是教学用的原子规则槽位,未完整实现第一篇中“包含不变量和多个变化点”的领域能力。真实领域能力应该由具有业务契约的服务或对象承担,不能把一个字符串 ID 当成全部领域模型。
本实验的优先级和金额阈值按规则槽位共享。修改一个槽位,会影响所有安装它的业务;不同业务需要不同参数时,应使用独立槽位或增加业务维度覆盖。Lattice 的公开配置则把冲突顺序放在具体业务的扩展点配置中。复用同一实现,不要求共享同一份可变配置。
请求输入只有业务身份与原始金额两个可变输入。固定的快照引用、事件列表、计算结果属于执行状态,不应重复算成配置要素。
表驱动如何建模
“表”首先表示可查询的关系,不要求数据库。Java Map、JSON、配置中心文档、关系表都可以表达这种结构。若要存进关系库,可采用下面的教学 schema;它没有声称任何厂商内部存在同名表。
1 | |
前三张表属于发布配置,最后一张是代码构件产生的目录。正式实现需要明确主键、外键或等价校验,以及配置版本与构件版本的兼容关系;仅有 release_id 不能保证旧代码认识新实现。
假定业务 presale 安装 base 和 presale 两个产品。基础产品提供公共校验、原价和审计;预售产品提供额外校验、预售价和额度处理。
图 3:从业务配置定位计价实现的教学关系。
flowchart LR
accTitle: 从业务配置定位计价实现的教学关系
B["业务 presale"] --> P1["产品 base"]
B --> P2["产品 presale"]
P1 --> C1["原价规则"]
P2 --> C2["预售价规则"]
C1 --> R1["实现 list-price / priority 0"]
C2 --> R2["实现 presale-price / priority 10"]
R1 --> EP["PRICE 候选集合"]
R2 --> EP
EP --> S["最高优先级候选"]
读取已发现对象上的注解,会建立包含实现 ID、扩展点和缺省标志的实现目录。沿业务到产品的关联查表,再限定哪些实现有资格参与。本次金额达到 minCents 后,候选才进入对应调用点。
Lattice 的真实配置也展示了分离:其公开 JSON 包含 bizCode、products[].code,以及按 extCode 配置的业务或产品优先顺序。本系列的三张 Map 采用更简化的结构,不与其 JSON 兼容。Lattice:Load Local Configuration。
注解、SPI和接口调用分别做什么
实验实际使用 JDK SPI 查找插件提供者:
1 | |
服务配置文件名为 META-INF/services/ExtensionLab$PluginProvider,文件内容为插件提供者的二进制类名 ExtensionLab$BasicPlugin。提供者返回实现对象,注册器再读取注解:
1 | |
因此这里没有“扫描整个 classpath 自动找到全部注解”的过程。发现提供者依靠 SPI,读取属性依靠反射,调用业务方法依靠普通接口分派。ServiceLoader 本身不理解业务身份、产品或优先级。JDK 8 ServiceLoader。
图 4:教学实现中 SPI 发现、注解登记与请求调用的时序。
sequenceDiagram
title 教学实现中 SPI 发现、注解登记与请求调用的时序
participant Boot as 启动代码
participant SPI as ServiceLoader
participant Provider as PluginProvider
participant Reg as Registry
participant Engine as Engine
participant Impl as Action实现
Boot->>SPI: 读取META-INF/services
SPI-->>Boot: 提供者实例
Boot->>Provider: actions()
Provider-->>Boot: 实现对象集合
Boot->>Reg: 注册对象
Reg->>Impl: 读取类上的Extension注解
Reg->>Reg: 校验重复ID与缺省实现
Note over Boot,Impl: 以上建立目录;还没有执行业务动作
Engine->>Reg: 按已发布配置取得候选
Reg-->>Engine: 元数据与实现对象
Engine->>Impl: apply(context)
若改用 Spring,可让容器发现并构造实现,再由注册器读取元数据;若改用注解处理器,可以在编译期生成目录或服务配置文件。两种方式都能减少手工注册,但它们并不会自动补上业务选择规则。
运行时注解读取还需要注意代理。示例对象是普通 Java 实例,所以直接读取 action.getClass()。若实例已被 AOP 代理,代理类未必具有目标类的注解,注册器需要按容器约定解析目标类型。把示例注册器直接搬进 Spring 而不处理这一点,可能注册不到实现。
接口、泛型与 Lambda 有不同作用:接口限定调用契约,泛型帮助表达参数与返回类型,Lambda 可以表达“对选中实现调用哪个方法”。反射提供动态发现手段,不要求业务调用阶段也使用 Method.invoke。代理则是在保持调用接口的同时插入处理逻辑,它既可以做本地路由,也可以作为 RPC 客户端入口。
类加载器会影响类型身份和插件生命周期,但一个独立 ClassLoader 并不自动提供资源隔离。插件启动的线程、静态缓存和外部注册若没有清理,替换插件包后仍可能留住旧对象。这已经超出最小注册表的职责。
扩展点句柄如何保持输入输出契约
调用方拿到的“句柄”可以是接口对象、扩展点 ID、带类型的 key,或者框架生成的代理。它用于定位一个调用契约,不一定固定指向某一个实现。业务身份可以在每次调用时改变选中的实现,但不应悄悄改变调用方以为自己正在调用的协议。
Java 的接口关系主要依靠名义类型
这里的“指称类型”通常称为名义类型(nominal typing)。两个类即使都有同名、同参数的方法,也不会因此自动实现同一个 Java 接口。实现方需要声明相应的 implements 关系,或通过继承满足它;框架也可以生成实现该接口的适配器或代理,但仍然需要真实的类型关系。
例如,一个独立设计的计价扩展点可以采用下面的声明。QuoteRequest 与 QuoteResult 是该契约专用的 DTO:
1 | |
一个只碰巧拥有 quote 方法、却没有实现 QuoteExtension 的类,不能直接赋给 QuoteExtension 引用。注解可以让框架发现这个类,但不能改变 Java 的赋值规则。类加载器也参与运行时类型身份:两个加载器各自加载同名接口,可能得到互不兼容的类型。因此插件系统通常需要共同使用平台契约所在的类加载边界。JLS:类型身份与子类型。
图 5:教学对照:同名方法不构成接口关系,同名接口由不同定义类加载器加载时也不是同一个运行时类型。
flowchart TB
accTitle: Java名义类型与定义类加载器的两道约束
subgraph Nominal["声明关系"]
Named["仅有同名quote方法"] --> Reject["不能赋给QuoteExtension"]
Declared["implements QuoteExtension"] --> Relation["建立名义子类型关系"]
end
subgraph Loader["运行时类型身份"]
Shared["共享契约的定义类加载器"] --> Common["调用方与插件使用同一接口Class"]
Split["两个加载器分别定义同名接口"] --> Different["得到两个不同接口Class"]
Different --> Fail["仅凭名称相同不能强转"]
end
Relation --> Shared
调用方与实现方共享的是契约包,框架选择的是实现对象。实现方可以返回声明结果类型的合法子类型,但不能把入参随意缩窄成只接受某个业务专用子类;那样通常会成为另一个重载方法,不能代替原接口要求的方法。若每个实现都把公共入参强转成自己的私有类型,说明公共契约没有表达真实需求。
Lattice Quickstart 就采用具体的方法契约。其扩展接口 OrderLinePriceExt 继承 IBusinessExt,声明 Long getCustomUnitPrice(OrderLine orderLine);能力类继承 BaseLatticeAbility<OrderLinePriceExt>,通过 reduceExecute 传入调用该方法的 lambda。公开示例保留了领域入参与结果类型,没有要求所有业务都变成 Object → Object。这证明调用处有静态类型约束,不代表已审查该版本框架内部所有强转;Quickstart 也未给出 reduceExecute 的完整泛型声明。Lattice Quickstart。
统一框架协议,不等于统一所有业务类型
框架层可以只抽象“输入到输出”,每个扩展点再绑定自己的类型:
1 | |
计价点可以是 Endpoint<QuoteRequest, QuoteResult>,资格点可以是 Endpoint<EligibilityRequest, Decision>。二者共享调用机制,不共享业务输入输出。泛型允许框架复用注册和调度代码,不能让计价结果在没有转换逻辑时成为库存预留命令。
一种教学用句柄是 Handle<I, O>,同时持有扩展点 ID、输入 Class<I> 和输出 Class<O>。调用 API 可以声明成:
1 | |
调用方拿到 Handle<QuoteRequest, QuoteResult> 后,正常的泛型调用可以让编译器检查输入,并推导输出为 QuoteResult。异构注册表仍可把多个扩展点放在一起;类型检查集中发生在注册与调用边界。
配套 TypedExtensionLab.java 演示了这种边界。它与业务组合实验分开,避免把“怎样选实现”与“选中后怎样保证类型”合成一个难以阅读的引擎。
它的实际调用代码保留了完整的类型关联:
1 | |
这里的 ID 只用于定位,输入输出类型来自句柄。注册器验证同一个 ID 不能另行声明成 String → String,也不接受与已定义契约不一致的句柄。这个小实验每个扩展点只绑定一个实现,未接入前一个实验的业务选择器;二者组合时,应先验证所有候选满足同一契约,再按业务规则选实现。
图 6:强类型句柄如何跨越异构注册表并恢复输入输出检查。
flowchart TB
accTitle: 强类型句柄如何跨越异构注册表并恢复输入输出检查
H["调用方句柄:quote<QuoteRequest, QuoteResult>"] --> Decl["定义表:ID + 输入Class + 输出Class"]
Impl["Endpoint<QuoteRequest, QuoteResult>实现"] --> Register["注册时核对契约与重复ID"]
Decl --> Register
Register --> Table["异构实现表:ID → CheckedInvoker"]
H --> Call["call:编译期关联输入输出"]
In["QuoteRequest对象"] --> Call
Call --> Table
Table --> InCheck["入口:input.cast + 非空检查"]
InCheck --> Run["调用已注册Endpoint"]
Run --> OutCheck["出口:output.cast + 非空检查"]
OutCheck --> Result["返回QuoteResult"]
casting 检查类型,转换器构造数据
注册表内部经常需要保存异构对象,框架可能用 Class.cast 恢复已登记的类型。下面是输入输出边界的核心形式:
1 | |
这是检查,不是数据转换。QuoteResult.class.cast(aString) 不会把字符串解析成报价结果,而是抛出 ClassCastException;cast(null) 则仍返回 null,所以非空契约必须另外校验。Class.cast。
若框架支持将 JSON、Map 或别的 DTO 转为目标类型,实际工作来自反序列化器、映射器或显式适配函数。转换器必须回答字段名、默认值、单位、枚举以及失败处理。例如把 amount=100 解释为 100 元还是 100 分,不能由强转解决。
反射也不绕过契约。Method.invoke 对接收者和参数有检查,并允许规定的方法调用转换;不兼容参数会失败。JDK 动态代理将调用交给 InvocationHandler,但处理器返回的结果仍须满足接口返回类型。把接口声明为 QuoteResult、处理器却返回字符串,仍会在代理调用处失败。Method.invoke、InvocationHandler.invoke。
泛型还有擦除边界。Class<List> 只能检查对象是不是 List,不能保证其中每个元素都是 QuoteResult。Type 或类型令牌可以记录更丰富的类型描述,但框架必须另行实现与该描述对应的检查或解码。教学句柄采用具体 DTO 的 Class,未实现嵌套泛型校验。JLS:类型擦除与可具体化类型。
擦除也不等于反射永远读不到泛型信息。类和方法声明可以保留泛型签名;这与一个普通 List 实例能够自行证明所有元素的类型,是两回事。
图 7:泛型边界示意:List.class 的容器检查通过后,元素类型仍需另行验证;具体检查逻辑不在本系列最小注册器中。
flowchart TB
accTitle: 泛型类型描述与集合元素检查的区别
Object["Object引用:一个List实例"] --> Container["List.class.cast"]
Container --> Pass["只能确认容器是List"]
Pass --> Element["若协议要求List of QuoteResult"]
Token["Type或类型令牌"] -->|提供期望类型描述| Element
Element --> Validate["框架逐项检查或按schema解码"]
Validate --> Valid{"所有元素满足契约?"}
Valid -->|是| Result["接受集合"]
Valid -->|否| Error["拒绝不兼容元素"]
图 8:类型检查与显式数据转换有不同的输入条件和结果。
flowchart TB
accTitle: 类型检查与显式数据转换有不同的输入条件和结果
subgraph Cast["Class.cast:检查已有对象"]
Existing["Object引用,目标是QuoteResult"] --> Compatible{"对象类型是否兼容?"}
Compatible -->|是| Same["返回同一对象引用"]
Compatible -->|否| Fail["ClassCastException"]
end
subgraph Convert["显式转换器:按协议创建目标对象"]
JSON["JSON / Map / 外部DTO"] --> Mapping["字段映射、单位转换、缺省值"]
Mapping --> Valid{"业务约束是否满足?"}
Valid -->|是| New["构造QuoteResult"]
Valid -->|否| Invalid["转换或校验失败"]
end
左侧假定输入非空;null 是否允许仍由扩展契约决定。右侧是工程设计对照,教学注册器没有内置 DTO 映射器。
一个可用的扩展协议至少约定哪些内容
不存在一份能够任意改变输入输出、同时让旧调用方保持安全的万能协议。可以固定框架外层调用形式,在每个扩展点内部保留明确的业务契约:
| 契约部分 | 计价扩展的教学约定 | 只靠注解能否保证 |
|---|---|---|
| 身份与版本 | quote.v1,绑定确定的输入输出类型 |
能登记标识,不能自动判断升级兼容 |
| 数据与语义 | 金额单位为分、币种明确、结果非负、是否允许 null | 类型只覆盖部分约束,还需业务校验 |
| 组合方式 | SINGLE 选一;若多选则定义结果归约 | 可声明策略,还需执行器实现 |
| 失败与副作用 | 区分不适用、拒绝、系统故障;计价是否纯计算 | 需实现遵约及契约测试 |
| 调用上下文 | 可信业务身份、配置版本;远程时另有超时和幂等约定 | 需入口、传播与调用层共同保证 |
输入输出变化可以分别处理。新增可选字段时,定义旧实现忽略它或采用哪个默认值;改变金额含义或必填条件时,考虑新扩展点版本或显式适配器;增加完全不同的业务动作时,定义另一个扩展点。JSON 能解码、Java 能编译,都不足以单独证明业务兼容。
图 9:教学决策:协议变化先检查语义兼容;适配器只适用于字段、单位和默认值有明确映射的情况。
flowchart TB
accTitle: 扩展协议输入输出变化的兼容处理
Change["输入输出发生变化"] --> Action{"仍是同一业务动作?"}
Action -->|否| NewPoint["定义另一个扩展点"]
Action -->|是| Compatible{"旧调用方与旧实现
仍满足原契约?"}
Compatible -->|是| Test["补充默认值与兼容测试"]
Compatible -->|否| Mapping{"存在明确且无歧义的映射?"}
Mapping -->|是| Adapter["显式适配器<br/>校验字段、单位与失败语义"]
Mapping -->|否| Version["发布新协议版本<br/>迁移调用方与实现方"]
统一 void apply(Context) 也是一种选择,本系列的组合实验就采用它。代价是每个阶段需要读取和写入哪些字段,没有完全体现在方法签名里。统一 Map 的自由度更大,但拼错字段、漏值和类型错误更容易推迟到运行时。若采用共享上下文,应补充阶段的数据契约;若采用强类型输入输出,流程编排器仍需验证上下游类型与语义能否衔接。
候选选择、执行顺序与归约不能混写
一次扩展调用可以用下面的过程解释:
1 | |
“优先级高”只说明排序,不能单独决定运行几个实现。调用点还必须定义组合语义。
| 语义 | 适用例子 | 必须明确的边界 |
|---|---|---|
| SINGLE | 从多个定价方案选一个 | 同优先级如何处理,没有候选怎么办 |
| FIRST 有效结果 | 依次尝试可选解析器 | 什么叫有效,异常是否允许继续 |
| ALL 顺序执行 | 按规则执行多个校验或装配动作 | 顺序、副作用、失败后是否停止 |
| 结果归约 | 全部通过、任一通过、取最小限购数 | 空集合结果、短路、重复元素、聚合顺序 |
Lattice 文档提供多种 Reduce 策略,说明组合可以包含返回值归约;它不等于简单的优先级排序。Lattice:Reduce Strategy。
本系列实验的 PRICE 采用 SINGLE,VALIDATE 与 INVENTORY 采用有序 ALL。ALL 通过共享 Context 记录结果,没有实现通用返回值归约器,也没有任意 DAG。
图 10:教学对照:同一候选集合在不同组合契约下会产生不同执行行为;右侧布尔归约不属于配套实验的已实现功能。
flowchart TB
accTitle: 候选排序之后SINGLE有序ALL与结果归约的区别
Candidates["已筛选且已排序的候选A、B、C"] --> Single["SINGLE"]
Candidates --> All["有序ALL"]
Candidates --> Reduce["示意:布尔AND归约"]
Single --> Unique{"最高优先级唯一?"}
Unique -->|是| One["只执行最高者"]
Unique -->|否| Conflict["报告冲突"]
All --> First["执行A"] --> Second["成功后执行B"] --> Third["成功后执行C"]
Reduce --> Booleans["收集或短路计算布尔结果"] --> Aggregate["按约定生成一个判定"]
缺省实现只处理“没有适用候选”。配置引用不存在的实现、最高优先级并列、实现内部抛异常,都不等于没有候选。如果这些错误也静默走缺省价,系统可能把失败变成错误但看似成功的订单。
配置可以热更新,请求不能读到半套关系
教学实验先验证候选快照中的引用,再通过 AtomicReference 一次替换已发布快照。请求进入时只读取一次快照,此后始终使用该引用。
图 11:教学实现的配置发布与请求快照。
sequenceDiagram
title 教学实现的配置发布与请求快照
participant Admin as 配置发布方
participant Engine as 执行引擎
participant Req as 请求
Admin->>Engine: 提交候选版本 v2
Engine->>Engine: 校验实现、关联与必需扩展点
alt 配置非法
Engine-->>Admin: 拒绝,继续使用 v1
else 配置合法
Engine->>Engine: 原子替换为 v2
Engine-->>Admin: 发布成功
end
Req->>Engine: 提交业务身份与金额
Engine->>Engine: 读取一次快照并保留到调用结束
Engine-->>Req: 结果、事件和配置版本
实验仍有明确限制:它在运行时检测 SINGLE 的同级冲突,没有在发布时完成全部条件重叠分析。生产系统应尽量提前拒绝可检测的歧义,同时保留运行时保护。规则条件一旦允许任意 Java 代码,完整静态判定往往不可行,需要限制条件语言或通过场景测试补充。
注解、配置和 Context 分开以后,变更范围也更清楚:新增实现要发布代码;用已有实现组合新业务可以发布配置;订单金额变化不需要改代码或配置。是否能够仅靠配置接入业务,取决于现有契约是否已经覆盖所需行为。
参考资料与运行入口
上述 Lattice 和 Camp 的具体 API 依据 2022 年公开示例;未将它们合并成一个可编译厂商项目。JDK 的注解与 SPI 行为按 Java 8 文档解释,配套实验也使用 Java 8。
第三篇 给出完整运行命令、输出、失败场景,以及远程扩展和 RPC 代理的对照。



