Skip to content

11 配置加载顺序:为什么你设置的超时时间不生效?

你好,我是何辉。今天我们探索Dubbo框架的第十道特色风味,配置加载顺序。

如何升级项目工程 pom 文件中某些 dependency 元素的版本号,想必你是轻车熟路了,一般情况下升级的版本都是向下兼容的,基本没问题,但如果跨越大版本升级,还是得多关注多验证一下,今天要解决的问题就是版本升级后出现的。

我们有这样一个敏感信息系统群,部分系统拓扑图如下:

图片

图中有提供方和消费方应用,都从 dubbo2 升级到了 dubbo3 版本,升级后放到测试环境验证了一圈都挺正常的,然而在发布日当晚,刚把系统发布到预发环境,就开始出现了一些消费方调用超时的现象,截取了一段异常日志:

Caused by: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2022-11-24 21:36:57.228, end time: 2022-11-24 21:36:58.246, client elapsed: 2 ms, server elapsed: 1016 ms, timeout: 1000 ms, request: Request [id=3, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=decrypt, parameterTypes=[class java.lang.String], arguments=[Geek], attachments={path=com.hmilyylimh.cloud.facade.crypto.CryptoFacade, remote.application=dubbo-11-loadcfg-consumer, interface=com.hmilyylimh.cloud.facade.crypto.CryptoFacade, version=0.0.0, timeout=1000}]], channel: /192.168.100.183:49527 -> /192.168.100.183:28110
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture.doReceived(DefaultFuture.java:212)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture.received(DefaultFuture.java:176)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.notifyTimeout(DefaultFuture.java:295)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.lambda$run$0(DefaultFuture.java:282)
    at org.apache.dubbo.common.threadpool.ThreadlessExecutor$RunnableWrapper.run(ThreadlessExecutor.java:184)
    at org.apache.dubbo.common.threadpool.ThreadlessExecutor.waitAndDrain(ThreadlessExecutor.java:103)
    at org.apache.dubbo.rpc.AsyncRpcResult.get(AsyncRpcResult.java:193)
    ... 29 more

异常信息中竟然是 timeout: 1000 ms,简直不可思议,明明解密系统在暴露接口的时候指定了超时时间,为什么就没生效呢?

我们去扒一下提供方和消费方的代码,看看设置的超时时间:

///////////////////////////////////////////////////
// 提供方:应用配置类,用 Java 代码的编写方式代替了以前 XML 编写配置
///////////////////////////////////////////////////
@Configuration
public class LoadCfgProviderConfig {
    // 提供者的应用服务名称
    @Bean
    public ApplicationConfig applicationConfig() {
        return new ApplicationConfig("dubbo-11-loadcfg-provider");
    }
    // 注册中心的地址,通过 address 填写的地址提供方就可以联系上 zk 服务
    @Bean
    public RegistryConfig registryConfig() {
        return new RegistryConfig("zookeeper://127.0.0.1:2181");
    }
    // 提供者需要暴露服务的协议,提供者需要暴露服务的端口
    @Bean
    public ProtocolConfig protocolConfig(){
        return new ProtocolConfig("dubbo", 28110);
    }
    // 提供者暴露接口的全路径为 com.hmilyylimh.cloud.facade.crypto.CryptoFacade 的服务过程
    @Bean
    public ServiceConfig<CryptoFacade> serviceConfigCryptoFacade(CryptoFacade cryptoFacade,
                                                                ApplicationConfig applicationConfig,
                                                                RegistryConfig registryConfig) {
        // 创建服务发布的配置对象,类比于 <dubbo:service/> 标签的效果
        ServiceConfig<CryptoFacade> serviceConfig = new ServiceConfig<>();
        // 设置需要暴露接口的全路径,类比于 <dubbo:service interface="com.hmilyylimh.cloud.facade.crypto.CryptoFacade"></dubbo:service> 标签中 interface 属性的效果
        serviceConfig.setInterface(CryptoFacade.class);
        // 设置需要暴露<dubbo:service ref="cryptoFacade"></dubbo:service> 标签中 ref 属性的效果
        serviceConfig.setRef(cryptoFacade);
        // 设置应用名称,类比于 <dubbo:application name="dubbo-11-loadcfg-provider"></dubbo:application> 标签的效果
        serviceConfig.setApplication(applicationConfig);
        // 设置注<dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry> 标签的效果
        serviceConfig.setRegistry(registryConfig);
        // 设置该 CryptoFacade 接口的默认超时时间为 5000 毫秒
        serviceConfig.setTimeout(5000);

        // 专门指定 CryptoFacade 中的 decrypt 方法超时时间为 3000 毫秒
        List<MethodConfig> methods = new ArrayList<>();
        MethodConfig methodConfig = new MethodConfig();
        methodConfig.setName("decrypt");
        methodConfig.setTimeout(3000);
        methods.add(methodConfig);
        serviceConfig.setMethods(methods);

        // 最终将 serviceConfig 对象导出服务(暴露服务过程)
        serviceConfig.export();
        return serviceConfig;
    }
}

///////////////////////////////////////////////////
// 提供方:加解密服务,目前只是实现了解密方法的实现逻辑
///////////////////////////////////////////////////
@Component
@DubboService
public class CryptoFacadeImpl implements CryptoFacade {
    @Override
    public String decrypt(String encryptContent) {
        // 睡眠 5000 毫秒,模拟解密耗时情况
        TimeUtils.sleep(5 * 1000);
        // 象征性的组装一下返回的明文结果
        String result = String.format("密文为: %s, 解密后的明文为: %s", encryptContent, "PLAIN-" + encryptContent);
        // 顺便在提供方打印一下返回的结果
        System.out.println(result);
        return result;
    }
}

///////////////////////////////////////////////////
// 提供方:应用启动类
///////////////////////////////////////////////////
@EnableDubbo
@SpringBootApplication
public class Dubbo11LoadcfgProviderApplication {
    public static void main(String[] args) {
        // 一行代码搞定 SpringBoot 应用的启动
        SpringApplication.run(Dubbo11LoadcfgProviderApplication.class, args);
        // 启动成功后,打印一下日志,方便肉眼可以直观的看到启动成功了
        System.out.println("【【【【【【 Dubbo11LoadcfgProviderApplication 】】】】】】已启动.");
    }
}

///////////////////////////////////////////////////
// 消费方:应用启动类
///////////////////////////////////////////////////
@ImportResource("classpath:dubbo-11-loadcfg-consumer.xml")
@SpringBootApplication
public class Dubbo11LoadcfgConsumerApplication {
    public static void main(String[] args) {
        // 一行代码搞定 SpringBoot 应用的启动
        ConfigurableApplicationContext ctx =
                SpringApplication.run(Dubbo11LoadcfgConsumerApplication.class, args);
        // 启动成功后,打印一下日志,方便肉眼可以直观的看到启动成功了
        System.out.println("【【【【【【 Dubbo11LoadcfgConsumerApplication 】】】】】】已启动.");
        // 然后模拟触发调用一下提供方的加解密服务
        CryptoFacade cryptoFacade = ctx.getBean(CryptoFacade.class);
        // 打印解密结果
        System.out.println(cryptoFacade.decrypt("Geek"));
    }
}

///////////////////////////////////////////////////
// 消费方:应用启动类的 @ImportResource 注解加载的
// dubbo-11-loadcfg-consumer.xml 配置文件内容
///////////////////////////////////////////////////
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xsi:schemaLocation="http://www.springframework.org/schema/beans        http://www.springframework.org/schema/beans/spring-beans-4.3.xsd        http://dubbo.apache.org/schema/dubbo        http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
    <!-- 消费者的应用服务名称 -->
    <dubbo:application name="dubbo-11-loadcfg-consumer"></dubbo:application>
    <!-- 注册中心的地址通过 address 填写的地址提供方就可以联系上 zk 服务 -->
    <dubbo:registry address="zookeeper://127.0.0.1:2181"></dubbo:registry>
    <!-- 引用加解密服务 -->
    <dubbo:reference id="cryptoFacade"
            interface="com.hmilyylimh.cloud.facade.crypto.CryptoFacade"></dubbo:reference>
</beans>

从提供方的暴露接口的代码来看,加解密服务的默认超时时间是 5000 毫秒(第37行),加解密服务中的解密方法设置的是 3000 毫秒(第43行),代码已经细化到方法级别来设置超时时间了,何况消费方也只是升级了 pom 文件的版本号,为什么发布到预发环境就超时了?太奇怪了。

这种现象,你会如何解决呢?

Debug 调试

现象摆在这了,在寻找解决方案之前,我们先来梳理下目前代码的现状,看看有没有突破口:

  • 提供方和消费方都升级了 Dubbo 版本号。
  • 提供方为加解密服务明确指定了超时时间,消费方代码也没有指定超时时间。
  • 消费方和提供方都按照未升级之前的老样子进行调用。

到这里还是没有头绪,到底是提供方没有读取到3000毫秒?或是消费方没有获取到3000毫秒?还是消费方获取到了3000毫秒但不使用呢?

这里我教你一个小技巧,对于一些不知如何分析的问题,可以尽量尝试 Debug 调试一下,调试的过程, 不但有助于你解决当下问题,还有助你更好地理解功能的调用流程。

既然消费方发生了超时异常,那我们就从刚才这段超时异常开始吧。细看超时异常的调用堆栈:

Caused by: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2022-11-24 21:36:57.228, end time: 2022-11-24 21:36:58.246, client elapsed: 2 ms, server elapsed: 1016 ms, timeout: 1000 ms, request: Request [id=3, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=decrypt, parameterTypes=[class java.lang.String], arguments=[Geek], attachments={path=com.hmilyylimh.cloud.facade.crypto.CryptoFacade, remote.application=dubbo-11-loadcfg-consumer, interface=com.hmilyylimh.cloud.facade.crypto.CryptoFacade, version=0.0.0, timeout=1000}]], channel: /192.168.100.183:49527 -> /192.168.100.183:28110
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture.doReceived(DefaultFuture.java:212)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture.received(DefaultFuture.java:176)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.notifyTimeout(DefaultFuture.java:295)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.lambda$run$0(DefaultFuture.java:282)
    at org.apache.dubbo.common.threadpool.ThreadlessExecutor$RunnableWrapper.run(ThreadlessExecutor.java:184)
    at org.apache.dubbo.common.threadpool.ThreadlessExecutor.waitAndDrain(ThreadlessExecutor.java:103)
    at org.apache.dubbo.rpc.AsyncRpcResult.get(AsyncRpcResult.java:193)
    ... 29 more

先是执行一个线程的 run 方法(RunnableWrapper.run),紧接着执行超时检测任务的通知超时方法(TimeoutCheckTask.notifyTimeout),最后执行了接收数据的核心方法(DefaultFuture.doReceived)。

乍一看,核心逻辑是在 DefaultFuture 中出现了异常,看到 Future 字样,相信你也想到了,在“ 异步化实践”中学过,Future 在获取结果时是支持传入超时时间的,那我们也来研究下 DefaultFuture 有没有超时参数。

看 DefaultFuture 的成员变量和一些方法的入参,在 DefaultFuture 的构造方法中发现了成员变量 timeout 的赋值逻辑:

// DefaultFuture 构造方法
private DefaultFuture(Channel channel, Request request, int timeout) {
    // 向外发送数据的通道,比如可以是 NettyClient 操作 netty 来发送数据
    this.channel = channel;
    // 该对象包含发送至提供方的所有数据
    this.request = request;
    // 能够表示请求对象的唯一ID
    this.id = request.getId();
    // 接收构造方法中传入的 timeout 参数值
    // 如果大于0则直接使用,否则直接从channel的url中获取
    this.timeout = timeout > 0 ? timeout :
          channel.getUrl().getPositiveParameter("timeout", 1000);
    // 构建唯一ID与当前对象的关系,唯一ID与发送数据通道的关系
    // put into waiting map.
    FUTURES.put(id, this);
    CHANNELS.put(id, channel);
}

构造方法支持 3 个参数的传入,其中一个就是 timeout 超时参数,如果 timeout 大于 0 ,直接使用,否则就从 channel 的 url 获取超时时间。

所以,接下来我们就要弄清楚 DefaultFuture 构造方法入参中的 timeout 是怎么计算出来的?

在刚刚找到的构造方法中,我们打上带条件的断点,来查看赋值 timeout 参数的源头在哪里:

图片

接着 Debug 运行一下消费方,当来到断点的时候:

图片

找到图中断点所在的调用堆栈,一路向下点击查看 timeout 传参的源头:

图片

最终在 DubboInvoker 的 doInvoke 方法中,你会找到 timeout 参数的局部变量,看图中红框的位置,可以看到该局部变量的值是通过一个 calculateTimeout 方法计算得到的。

到目前为止,我们还没有找到可能造成超时异常问题的疑点,不过也别着急,毕竟计算 timeout 的源头在方法calculateTimeout里面,说明它还是挺关键的,我们不妨大致浏览下这个类的变量和方法,混个眼熟。

来看下 DubboInvoker 的成员变量和一些方法的入参,发现有个 Invoker 接口的集合 invokers 成员变量,而当前的 DubboInvoker 也是 Invoker 接口的子类,它们之间有什么关系呢?我们再来查看一下 invokers 内容:

图片

奇怪的是,看红框可以发现当前 DubboInvoker 的 this 引用在 invokers 当中,而且 invokers 中的另外两个元素也是 DubboInvoker 类型的,我们根本看不出这3个元素有什么区别,难道 Dubbo 框架随机选择了 invokers 中的一个元素进行调用的么?

我们不妨继续展开 invokers 中的每个元素的成员变量:

图片

可以很清楚地看到当前的 Invoker 引用,和其他几个引用中的 url 变量结构是不一样,为什么不一样呢?目前还是看不出什么端倪。

不过,可以发现 url 变量是从 DubboInvoker 的构造方法传入进来的,那我们继续在 DubboInvoker 的构造方法中打上条件断点,看看赋值 url 的源头到底是谁:

图片

再次 debug 运行一下消费方,关注调用堆栈,看看有什么不一样的地方:

图片

发现断点进入了多次,每次 invokers 都会增加一个,奇怪的是,有 1 次调用堆栈显示了 ServiceDiscoveryRegistryDirectory 这么一个类,这不就是 Dubbo 新版本的服务发现应用级订阅?而且当前 DubboInvoker 的 this 引用正好是这个服务发现的引用,难道 Dubbo 框架从 invokers 中选择了新版本的服务发现实例么?

两个可疑点

通过几次断点分析,我们从 DubboInvoker 中找到了两个引起 timeout 不能正确赋值的可疑点:

  1. calculateTimeout 为什么没有得到正确的值?可以怎样得到正确的值呢?
  2. 为什么使用的是新版本的服务发现类进行远程调用呢?

接下来,我们着重分析下这两个可疑点。

可疑点一

先分析可疑点一,首先要进入 calculateTimeout 方法看下具体逻辑,这里的逻辑比较深,不过没关系,我总结这个方法内部大致的代码调用流程,你可以参考:

图片

调用流程整体分为三大块, 先取方法级别的参数,再取服务级别的参数,最后取实例级别的参数;在每一块的内部 按照先取消费方,再提供方的顺序读取参数

由于目前 Debug 运行时使用的 DubboInvoker 是新版本的服务发现实例,而且提供方加解密服务的 @DubboService 注解中没有配置任何超时时间,消费方的 XML 配置文件中也没有配置任何超时时间,这样一来,正好符合图中拿不到任何 timeout 参数值的现象,也就是说,拿到的是一个 null 。

如果你对这个结论抱着怀疑的态度,大可以放心去服务方加解密服务的 @DubboService 注解中修改下:

@DubboService(timeout = 2800, methods = {@Method(
        name = "decrypt",
        timeout = 2400,
        parameters = {
                "timeout", "2000"
        })}
)

在 @DubboService 注解中,为 decrypt 进行服务级别配置 timeout = 2800,方法级别配置 timeout = 2400,方法级别的参数配置 timeout = 2000。

最终,再启动提供方和消费方,你会看到消费方的超时异常日志中提示目前的超时时间 timeout = 2000。

// 在提供方添加 timeout = 2800、timeout = 2400、timeout = 2000 里三个超时时间
// 再次运行消费方后,看到的异常信息如下:
Caused by: org.apache.dubbo.remoting.TimeoutException: Waiting server-side response timeout by scan timer. start time: 2022-11-25 23:20:46.834, end time: 2022-11-25 23:20:49.053, client elapsed: 1 ms, server elapsed: 2218 ms, timeout: 2200 ms, request: Request [id=3, version=2.0.2, twoway=true, event=false, broken=false, data=RpcInvocation [methodName=decrypt, parameterTypes=[class java.lang.String], arguments=[Geek], attachments={path=com.hmilyylimh.cloud.facade.crypto.CryptoFacade, remote.application=dubbo-11-loadcfg-consumer, interface=com.hmilyylimh.cloud.facade.crypto.CryptoFacade, version=0.0.0, timeout=2200}]], channel: /192.168.100.183:64923 -> /192.168.100.183:28110
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture.doReceived(DefaultFuture.java:212)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture.received(DefaultFuture.java:176)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.notifyTimeout(DefaultFuture.java:295)
    at org.apache.dubbo.remoting.exchange.support.DefaultFuture$TimeoutCheckTask.lambda$run$0(DefaultFuture.java:282)
    at org.apache.dubbo.common.threadpool.ThreadlessExecutor$RunnableWrapper.run(ThreadlessExecutor.java:184)
    at org.apache.dubbo.common.threadpool.ThreadlessExecutor.waitAndDrain(ThreadlessExecutor.java:103)
    at org.apache.dubbo.rpc.AsyncRpcResult.get(AsyncRpcResult.java:193)
    ... 29 more

对于这样的流程,回想你写代码配置服务级别或方法级别的参数时,有没有一种似曾相识的感觉,没错,都是遵循着“粒度越细,优先级越高”的方式来处理的。

严谨的你可能疑惑,@DubboService 中有三个地方(服务级别、方法级别、方法参数)可以添加超时时间,消费方 @DubboReference 注解也有三个地方可以配置,还有 dubbo.properties、启动命令-D参数这些地方都可以配置,那, 到底该以谁的配置为准呢

这个 calculateTimeout 计算超时时间的逻辑问题,其实 Dubbo 框架早就埋好了伏笔,在 Dubbo 3.0 开发手册 Java -> 参考手册 -> 配置说明 -> 配置工作原理 -> 属性覆盖 为我们详细描述了不同层级配置之间的属性覆盖关系,不管是在消费方,还是在提供方,各自都按照这样的层级覆盖关系,然后在 calculateTimeout 方法中就能从消费方、提供方取到精准的超时时间。

官网覆盖关系图 的基础之上,我们看看常用的配置写法,如图:

图片

主要有四个层级关系:

  • System Properties,最高优先级,我们一般会在启动命令中通过 JVM 的 -D 参数进行指定,图中通过 -D 参数从指定的磁盘路径加载配置,也可以从公共的 NAS 路径加载配置。
  • Externalized Configuration,优先级次之,外部化配置,我们可以直接从统一的配置中心加载配置,图中就是从 Nacos 配置中心加载配置。
  • API / XML / 注解,优先级再次降低,这三种应该是我们开发人员最熟悉不过的配置方式了。
  • Local File,优先级最低,一般是项目中默认的一份基础配置,当什么都不配置的时候会读取。

看完四层覆盖关系,想必你已经知道如何解决超时不生效的问题了。

参考今天问题中的具体情况,因为升级了 Dubbo 版本,消费方自己没配置任何超时时间,提供方使用了 API 方式设置超时时间,但是消费方并不感知,反而消费方感知的是提供方 @DubboService 中配置的内容,那就意味着 只要提供方有自己一套默认的超时时间,那消费方就会直接使用提供方的超时时间进行远程调用了

所以,我们可以尝试在提供方的 resources 资源目录下新增一个 dubbo.properties 文件,配上默认的超时时间为 5000 毫秒:

# 在提供方工程中的 resources/dubbo.properties 增加默认超时时间
dubbo.provider.timeout=5000

这样一来提供方就有了自己默认的一套超时时间了,消费方也不会走默认的 1000 毫秒超时逻辑了。

当然刚刚采用的是 Local File 优先级最低的方式进行配置的,你还可以通过其他三种方式进行配置,总之,哪种你使用起来最方便,又最适合你们目前项目的紧急诉求,配置哪种就行了。

可疑点二

虽然这个问题暂时解决了,但可疑点二我们还不知道原因,为什么使用的是新版本的服务发现类进行远程调用呢?

还记得我们在“ 温故知新”中在提供方和消费方中都提到过一个概念吗,应用级注册和接口级注册。提供方这边默认情况下,不但会进行应用级注册,还会进行接口级注册;而在消费方这边,有个智能决策的兼容过度方案,优先使用应用级注册信息。

所以,可疑点二这个问题,估计就是因为提供方走进了默认的注册服务策略,消费方那边又恰好采用的是智能决策策略,就变成使用新版本服务发现类进行远程调用了。

说到这,想必你已经想到如何应对了。我们可以在消费方设置只订阅接口级注册:

# 在消费方工程中的 resources/dubbo.properties 增加只订阅接口级注册
dubbo.application.service-discovery.migration=FORCE_INTERFACE

也可以在提供方设置只进行接口级注册:

# 在提供方工程中的 resources/dubbo.properties 增加只进行接口级注册
dubbo.application.register-mode=interface

这样一来,虽然升级到了 Dubbo3 版本,但其实还是走着 Dubbo2 旧的分支逻辑,这也侧面佐证了 Dubbo3 对 Dubbo2 的向下兼容友好特性。

不同层级配置的适用场景

从 Dubbo 框架的四层属性覆盖关系可以看出,一个简单的配置居然有如此之多的配置来源,那它们的应用场景有什么区别么?

System Properties,一般是固定不能被覆盖的参数,这些参数基本上不会变化,由运维人员按照公司的标准化 JVM 启动参数统一控制应用。

Externalized Configuration,一般是偏应用系统层面的公共参数,可以从公共的配置中心读取,这些参数很大程度上也不会经常变化,一旦遇到突发情况或想统一修改公共参数,可以在不改代码的情况下,通过重启或动态加载最新配置。

API / XML / 注解,这种属于开发层面比较个性化的配置方式了,主要是因为实际开发的项目中,不同的功能,需要根据实际情况合理配置不同的参数。

Local File,这种属于兜底级别了,如果其他层级都没有配置的话,至少还有个当前系统全局默认的兜底配置。

总结

今天,从系统跨越版本升级而引发的超时时间失效的问题开始,我们针对代码的三个现状,开启了 Debug 调试分析之旅,简单几步调试之后,找到了 timeout 无法获取预期值的源头,同时也发现了破解问题的两个关键可疑点:

  • 从可疑点一中,仔细研读计算超时时间的方法,挖掘出了 Dubbo 配置的四层覆盖关系,最终我们巧妙利用不同层级的特性,解决了超时时间失效的问题。
  • 从可疑点二中,发现类名使用了新版本的服务发现类进行远程调用,联想到了应用级注册和接口级注册的因素,最终也找到了解决超时时间失效的方案。

消费方在发起远程调用时,超时时间的取值逻辑要记牢:

  • 首先,整体分为三大块,分别为方法级别、服务级别、实例级别。
  • 然后,在每一块的内部,按照先消费方后提供方的的顺序进行取值。

Dubbo 配置的四层覆盖关系,优先级从高到低依次为:System Properties、Externalized Configuration、API / XML / 注解、Local File。

最后如果提供方和消费方按照接口级注册使用,提供方指定注册模式(dubbo.application.register-mode)为接口级(interface),消费方服务发现迁移模式(dubbo.application.service-discovery.migration)为接口级(FORCE_INTERFACE)。

思考题

留个作业给你,在工作中,你会需要使用外部配置中心来存储配置,在 Nacos 中很可能会出现这样的报错场景:

图片

再次新建一条记录,内容为 dataId = dubbo.properties,group = dubbo,添加完再点击提交就报错了,你知道这是为什么吗?

公司里面那么多应用系统,如果按照图中这样配置所有应用都使用同一份 dubbo.properties 属性内容,未免有点糟糕,该怎么遵循 Nacos 的规范正确填写 标签里面的内容呢?

欢迎在留言区分享你的思考和学习心得。我们下节课再见。

10思考题参考

上一期留了一个作业,TokenFilter、ConsumerSignFilter、ProviderAuthFilter 这三个过滤器是干什么用的,以及如何应用?

首先进入 TokenFilter 的源码底层看看:

/**
 * Perform check whether given provider token is matching with remote token or not. If it does not match
 * it will not allow invoking remote method.
 *
 * @see Filter
 */
@Activate(group = CommonConstants.PROVIDER, value = TOKEN_KEY)
public class TokenFilter implements Filter {
    @Override
    public Result invoke(Invoker<?> invoker, Invocation inv)
            throws RpcException {
        // 参数一:可以设置 token 参数
        String token = invoker.getUrl().getParameter("token");
        // 如果设置有值,那么就直接和接收的 token 两两做比较
        if (ConfigUtils.isNotEmpty(token)) {
            Class<?> serviceType = invoker.getInterface();
            Map<String, Object> attachments = inv.getObjectAttachments();
            // 从接收的参数中获取消费方发来的 token 值
            String remoteToken = (attachments == null ? null : (String) attachments.get("token"));
            if (!token.equals(remoteToken)) {
                // 如果比对不一致的话,则抛出异常
                throw new RpcException("Invalid token! Forbid invoke remote service " + serviceType + " method " + inv.getMethodName() +
                        "() from consumer " + RpcContext.getServiceContext().getRemoteHost() + " to provider " +
                        RpcContext.getServiceContext().getLocalHost()+ ", consumer incorrect token is " + remoteToken);
            }
        }
        return invoker.invoke(inv);
    }
}

阅读 TokenFilter 源码,可以得出 2 点信息:

  1. TokenFilter 只作用在提供方。
  2. 提供方可以通过参数 token 来配置值,如果消费方没传 token,或者消费方传错了 token,最终都会导致提供方这边校验不通过,一旦不通过就会抛出异常。

不难看出,其实 TokenFilter 就是来做鉴定真假的,和我们之前写的 ProviderTokenFilter 思路非常相似。

然后进入 ConsumerSignFilter 的源码底层看看:

@Activate(group = CommonConstants.CONSUMER, value = Constants.SERVICE_AUTH, order = -10000)
public class ConsumerSignFilter implements Filter {
    private ApplicationModel applicationModel;
    public ConsumerSignFilter(ApplicationModel applicationModel) {
        this.applicationModel = applicationModel;
    }
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        // 参数一:可以设置 auth 参数,看看要不要开启认证功能
        boolean shouldAuth = url.getParameter("auth", false);
        if (shouldAuth) {
            // 如果开启了认证功能的话,那么就直接将请求参数进行加签
            // 然后放到 invocation 中的 attachments 中,顺路传到提供方去
            Authenticator authenticator = applicationModel.getExtensionLoader(Authenticator.class)
              .getExtension(url.getParameter("authenticator", "accesskey"));
            // 通过 url 获取的默认扩展点进行加签操作
            // 当然既然是从 url 获取的扩展点,那么就提供了自定义的扩展口子
            authenticator.sign(invocation, url);
        }
        return invoker.invoke(invocation);
    }
}

可以得出 3 点信息:

  1. ConsumerSignFilter 只作用在消费方。
  2. 有个 auth 参数来决定是否开启加签操作,如果开启了那就进行加签,并把加签后的值顺道传给提供方。
  3. 加签的核心逻辑是可以扩展的,通过设置 authenticator = xxx 就可以实现自定义加签逻辑。

ConsumerSignFilter 就是来做加签操作的,和我们之前写的 ConsumerAddSignFilter 思路非常相似。

最后进入 ProviderAuthFilter 的源码底层看看:

@Activate(group = CommonConstants.PROVIDER, order = -10000)
public class ProviderAuthFilter implements Filter {
    private ApplicationModel applicationModel;
    public ProviderAuthFilter(ApplicationModel applicationModel) {
        this.applicationModel = applicationModel;
    }
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        URL url = invoker.getUrl();
        // 参数一:可以设置 auth 参数,看看要不要开启认证功能
        boolean shouldAuth = url.getParameter("auth", false);
        if (shouldAuth) {
            // 如果开启了认证功能的话,那么就直接进行验签操作
            Authenticator authenticator = applicationModel.getExtensionLoader(Authenticator.class)
              .getExtension(url.getParameter("authenticator", "accesskey"));
            try {
                // 通过 url 获取的默认扩展点进行验签操作
                // 当然既然是从 url 获取的扩展点,那么就提供了自定义的扩展口子
                authenticator.authenticate(invocation, url);
            } catch (Exception e) {
                return AsyncRpcResult.newDefaultAsyncResult(e, invocation);
            }
        }
        return invoker.invoke(invocation);
    }
}

可以得出 3 点信息:

  1. ProviderAuthFilter 只作用在提供方。
  2. 也有个 auth 参数来决定要不要开启验签操作,如果开启了那么进行验签,验签不通过的话则会直接抛出异常。
  3. 验签核心逻辑也是可以支持扩展的,通过设置 authenticator = xxx 就可以实现自定义验签逻辑。

我们也不难得出,ConsumerSignFilter、ProviderAuthFilter 两个是成对存在的,前者负责加签,后者负责验签,和我们之前写的 ConsumerAddSignFilter、ProviderVerifySignFilter 思路也非常相似。