文章

从Dubbo看框架配置设计:默认行为之上的按需编排

从Dubbo看框架配置设计:默认行为之上的按需编排

1. 背景

Dubbo为常见场景提供了开箱即用的默认配置,应用不必从零组装基础能力,也可以通过SPI接入自己的实现。以Filter链为例,应用可能希望保留大部分默认行为,只增加一个Filter、排除某个Filter或调整执行顺序。围绕这些需求,我们从启停开关出发,逐步讨论扩展配置的设计。

2. Filter链与关注点分离

一次RPC调用除了执行业务逻辑,还经常需要记录日志、校验身份和采集指标。Dubbo用独立的Filter承载这些处理,再按顺序连接成链。日志、鉴权、监控各管一件事,可以独立维护和扩展,这就是关注点分离。

每个Filter都接收请求,以及代表后续链路的Invoker。需要继续调用时,通过next.invoke把请求交给后续节点;鉴权失败时,也可以直接返回或抛出异常。节点之间通过next衔接,形成责任链结构。

用log、auth、metrics表示日志、鉴权和监控三个Filter,正常继续处理时,请求沿下图向后传递:

请求沿 Dubbo Filter 链依次经过 log、auth、metrics,最后到达目标 Invoker

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。框架以后增加适用的默认扩展时,这些扩展也能自动加入,无须修改这份配置。

不过,这份配置还无法表达前面要求的“先审计、再鉴权”:

audit 追加到末尾与放到最前时的 Filter 链对比

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已经被单独安排,就不能再从默认列表里加入一次。合并过程如下:

metrics,default,-auth 展开并合并为最终列表的过程

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)。应用可以只写对默认列表的调整,也可以明确接管整个列表。只改少数成员时,不必把整份默认列表抄过来维护。

本文由作者按照 CC BY 4.0 进行授权