从Dubbo看框架配置设计:默认行为之上的按需编排
1. 背景
Dubbo为常见场景提供了开箱即用的默认配置,应用不必从零组装基础能力,也可以通过SPI接入自己的实现。以Filter链为例,应用可能希望保留大部分默认行为,只增加一个Filter、排除某个Filter或调整执行顺序。围绕这些需求,我们从启停开关出发,逐步讨论扩展配置的设计。
2. Filter链与关注点分离
一次RPC调用除了执行业务逻辑,还经常需要记录日志、校验身份和采集指标。Dubbo用独立的Filter承载这些处理,再按顺序连接成链。日志、鉴权、监控各管一件事,可以独立维护和扩展,这就是关注点分离。
每个Filter都接收请求,以及代表后续链路的Invoker。需要继续调用时,通过next.invoke把请求交给后续节点;鉴权失败时,也可以直接返回或抛出异常。节点之间通过next衔接,形成责任链结构。
用log、auth、metrics表示日志、鉴权和监控三个Filter,正常继续处理时,请求沿下图向后传递:
3. 框架的默认选择
职责拆开以后,还需要决定哪些Filter进入链。SPI提供可用实现,框架根据当前场景选择成员并排序,让应用不写额外配置也能使用。这份由框架选出的有序扩展列表,就是默认列表。
例如,某个Filter只用于服务端,另一个只有开启监控时才加入。同一套依赖在不同条件下,默认列表也可能不同。应用写入的扩展名和控制标记称为显式配置。框架处理配置后,用来组装链的那份列表称为最终列表。
假设当前默认列表为[log, auth, metrics]。应用还注册了audit(审计)和statistics(统计),但它们不会自动加入。后面的例子都从这份默认列表出发。
4. 从启停控制到完整列表
某个应用已经在其他位置记录了日志,不需要这条链再执行log。一个启停开关就能满足:
1
log.enabled=false
1
2
原列表:[log, auth, metrics]
关闭log后:[auth, metrics]
接着,应用增加了audit,希望在鉴权之前留下请求记录,这样即使鉴权失败,也能查到这次请求。开关只能决定是否启用它,还需要一个办法把它放到链头。
一种办法是增加顺序参数,但成员和位置就分散在了不同字段里。另一种办法是用完整列表同时指定成员和顺序:
1
filters=audit,auth,metrics
按这套规则,只使用列出的扩展,并按书写顺序组装。log没有列出,因此不加入;audit排在auth前面,满足先审计、再鉴权的要求。
但应用也必须写出自己并不打算改变的auth和metrics。以后框架新增了适用于当前场景的基础能力,这份配置也不会自动包含它。只是去掉log、增加audit,应用却需要维护整个列表。
5. 只配置与默认列表的差异
如果大部分成员仍然沿用框架的选择,显式配置就可以只记录差异:哪些要增加,哪些要排除。
于是换一种解释配置的方式:普通扩展名表示加入,-name表示排除;没有显式配置时,沿用默认列表。暂时把显式指定的扩展追加到默认列表末尾:
1
filters=-log,audit
对于这份配置,可以从空的最终列表开始,先加入默认列表,再移除log、追加audit:
1
2
3
4
5
6
7
List<String> defaultFilters = List.of("log", "auth", "metrics");
List<String> result = new ArrayList<>();
result.addAll(defaultFilters); // 先加入默认列表
result.remove("log"); // -log:排除log
result.add("audit"); // audit:加入显式指定的扩展
// 最终列表:[auth, metrics, audit]
应用只需维护-log,audit,不用再列出auth和metrics。框架以后增加适用的默认扩展时,这些扩展也能自动加入,无须修改这份配置。
不过,这份配置还无法表达前面要求的“先审计、再鉴权”:
6. default:为默认列表安排位置
要让audit先执行,又不重新枚举其余成员,就需要在显式配置中引用默认列表。用default代表它,位置便可以直接写在配置里。
下面看两个场景:在默认列表前后插入新扩展,以及把默认列表中的某个扩展单独安排到前面。
6.1 在默认列表前后插入新扩展
保留-log,把audit放在默认列表前面。如果还要在末尾加入statistics,可以一起写出来:
1
filters=-log,audit,default,statistics
排除log后,default展开为[auth, metrics],最终列表是[audit, auth, metrics, statistics]。应用决定前后放什么,中间的成员仍由框架选择。
6.2 调整已有扩展的位置
另一个场景希望把默认列表中的metrics提前,同时排除auth:
1
filters=metrics,default,-auth
metrics已经被单独安排,就不能再从默认列表里加入一次。合并过程如下:
default承接的是剩余默认列表:去掉被排除的成员,以及已被显式配置安排的成员。显式指定的成员先退出默认列表,再按配置位置加入,避免重复。
“剩余”只在默认列表内计算,不包含所有尚未被指定的SPI扩展。
7. 禁用默认列表,完全自定义
如果应用只需要audit和statistics,仅写这两个名字还不够:默认列表仍会加入。
可以用-default禁用默认列表,再指定要使用的成员:
1
filters=-default,audit,statistics
这时最终列表只有[audit, statistics]。
沿用默认列表时,应用只维护自己的调整,其余成员随框架的默认选择变化。使用-default后,列表由应用自己维护,框架新增的默认扩展不会自动加入。
8. Dubbo中的配置解析与Filter链组装
这套规则对应Dubbo的getActivateExtension:它负责筛选并合并扩展,调用方再用最终列表组装Filter链。下面用Java伪代码拆开这几个步骤。
8.1 声明默认加入条件
先让SPI登记名字与实现。伪代码用@Condition表达默认加入条件,作用类似Spring的条件装配;Dubbo中的对应注解叫@Activate。
1
2
3
4
5
SPI.register("log", LogFilter.class);
SPI.register("auth", AuthFilter.class);
SPI.register("metrics", MetricsFilter.class);
SPI.register("audit", AuditFilter.class);
SPI.register("statistics", StatisticsFilter.class);
1
2
3
4
5
6
7
8
9
10
11
12
@Condition(always = true, order = 10)
class LogFilter implements Filter { /* ... */ }
@Condition(side = "provider", order = 20)
class AuthFilter implements Filter { /* ... */ }
@Condition(parameter = "metrics.enabled", order = 30)
class MetricsFilter implements Filter { /* ... */ }
// 没有默认加入条件,由显式配置选择
class AuditFilter implements Filter { /* ... */ }
class StatisticsFilter implements Filter { /* ... */ }
假设当前是服务端,并且metrics.enabled已开启,筛选并排序后就能得到[log, auth, metrics]。后续代码中的context表示这些场景条件,SPI.get(name)取得对应的Filter实例。
8.2 计算剩余默认列表
继续使用metrics,default,-auth这个例子。显式配置按逗号拆成configItems,交给selectRemainingDefaults,得到未被排除、也未被单独安排的剩余默认列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
List<Filter> selectRemainingDefaults(List<String> configItems, Context context) {
List<Filter> remainingDefaults = new ArrayList<>();
// 应用禁用了默认列表,直接返回空列表。
if (configItems.contains("-default")) {
return remainingDefaults;
}
for (var extension : SPI.extensions(Filter.class)) {
String name = extension.name();
boolean excluded = configItems.contains("-" + name);
boolean explicitlySelected = configItems.contains(name);
// 被排除的不加入;已显式指定的留到合并阶段安排,避免重复。
if (excluded || explicitlySelected) {
continue;
}
// 只有声明了默认加入条件、且当前场景满足条件的扩展才加入。
if (extension.hasCondition() && extension.matches(context)) {
remainingDefaults.add(SPI.get(name));
}
}
// 默认扩展之间仍沿用框架声明的顺序。
remainingDefaults.sort(byOrder());
return remainingDefaults;
}
matches(context)检查条件,byOrder()按声明的顺序排序。本例中,metrics留到合并阶段安排,auth被排除,方法返回[log]。
8.3 合并显式指定的扩展
mergeExplicitFilters接收剩余默认列表和配置项,返回最终列表。显式指定的扩展先暂存在explicitFilters中:遇到default就插到剩余默认列表前面;遍历结束,再把暂存的剩余扩展追加到末尾。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
List<Filter> mergeExplicitFilters(List<Filter> remainingDefaults, List<String> configItems) {
List<Filter> result = new ArrayList<>();
result.addAll(remainingDefaults);
List<Filter> explicitFilters = new ArrayList<>();
for (String name : configItems) {
// -name是排除标记,不加载;被排除的扩展也不能通过显式配置加入。
if (name.startsWith("-") || configItems.contains("-" + name)) {
continue;
}
if ("default".equals(name)) {
// default之前积累的扩展,放在剩余默认列表前面。
result.addAll(0, explicitFilters);
explicitFilters.clear(); // 已加入最终列表,避免末尾再次追加。
} else {
// 先按配置顺序暂存,等待default或遍历结束决定位置。
explicitFilters.add(SPI.get(name));
}
}
// default之后的扩展放到最后;没有default时,所有显式扩展都追加到最后。
result.addAll(explicitFilters);
return result;
}
本例中,result先包含log;读取metrics时暂存,读到default时将它插到log前,-auth跳过,得到[metrics, log]。这里通过SPI.get(name)直接取得显式指定的扩展,不再检查默认加入条件。
两段代码都会检查-name,所以同一个名字同时被加入和排除时,排除优先,与配置项的先后顺序无关。
8.4 按最终列表组装Filter链
最终列表交给buildFilterChain。它从后往前包装Invoker,让列表靠前的Filter处在链的外层。
1
2
3
4
5
6
7
8
9
10
Invoker buildFilterChain(List<Filter> filters, Invoker targetInvoker) {
Invoker chain = targetInvoker;
// 倒序包装:越靠前的Filter越晚包装,因而位于越外层。
for (int i = filters.size() - 1; i >= 0; i--) {
Filter filter = filters.get(i);
chain = wrap(filter, chain); // 当前Filter的next指向已组装好的后续链。
}
return chain;
}
targetInvoker负责实际调用,wrap(filter, chain)把当前Filter包在已有链外。对于[metrics, log],先包log,再包metrics;请求进入时却是从外向内,先经过metrics,再经过log,最后到达目标Invoker。
8.5 串起完整的组装过程
三个方法连起来,配置就变成了一条可以执行的Filter链:
1
2
3
4
5
6
7
Invoker createFilterChain(String config, Context context, Invoker targetInvoker) {
List<String> configItems = splitConfig(config); // 按逗号拆分显式配置。
List<Filter> remainingDefaults = selectRemainingDefaults(configItems, context);
List<Filter> filters = mergeExplicitFilters(remainingDefaults, configItems);
return buildFilterChain(filters, targetInvoker);
}
筛选与合并决定最终列表,组装决定节点之间怎样连接。前两个方法对应getActivateExtension的职责,组装链由调用方完成。实际配置项是服务端的dubbo.provider.filter和调用方的dubbo.consumer.filter,前面的filters用于简写。
9. 总结
回看前面的推演,每种配置写法都对应一个具体问题:
| 希望解决的问题 | 方案如何变化 |
|---|---|
| 只想关闭某个扩展 | 使用启停开关,控制单项能力。 |
| 既要增加扩展,又要安排顺序 | 改用完整列表,同时表达成员与顺序。 |
| 只改少数成员,却要维护整个列表 | 保留默认列表,普通名字表示加入,-name表示排除。 |
| 想在默认扩展前后插入,又不想枚举全部成员 | 引入default,让剩余默认列表可以被整体引用和定位。 |
| 只使用自己指定的扩展 | 通过-default禁用默认列表,完全按显式配置选择。 |
扩展名、-name、default和-default组合起来,就是一套小型领域专用语言(DSL)。应用可以只写对默认列表的调整,也可以明确接管整个列表。只改少数成员时,不必把整份默认列表抄过来维护。


