接着《Dubbo源码深度解析(一)》继续讲,上篇博客主要讲Dubbo提供的三个注解的作用,即:@EnableDubbo、@DubboComponentScan、@EnableDubboConfig。其中后两个注解是在@EnableDubbo上的,因此在启动类上加上@EnableDubbo注解,等同于加上@DubboComponentScan注解和@EnableDubboConfig注解。并且还讲到了Dubbo的包扫描,以及Dubbo整合SpringBoot后,是如何将配置文件中的dubbo.xxx属性绑定到Dubbo的配置类上的。
本篇博文将主要讲@DubboService注解的原理以及Dubbo的SPI机制,其实@DubboService注解的原理,在上篇博文中已经讲过了。这里回顾一下:核心是依赖ServiceClassPostProcessor或者ServiceAnnotationBeanPostProcessor,其中,ServiceAnnotationBeanPostProcessor继承自ServiceClassPostProcessor(非抽象类),都是实现了BeanDefinitionRegistryPostProcessor接口。因此会实现BeanDefinitionRegistryPostProcessor#postProcessBeanDefinitionRegistry()方法,在该方法中,会扫描指定包下所有被@DubboService注解修饰的类,并往Spring容器中注册一个BeanDefinition对象,其中BeanDefinition的beanClass的值为ServiceBean.class,最终Spring实例化的就是beanClass返回的类型,即ServiceBean,上篇博文最后通过AbstractApplicationContext#getBeansOfType(ServiceBean.class)方法,能获取到一个Bean对象,这跟预期是一致的。
其实在生成ServiceBean的时候,会用到Dubbo的配置类,在子模块service-provider中,我通过DubboConfiguration,定义了四个配置类,分别是:ApplicationConfig、ProtocolConfig、RegistryConfig和ConfigCenterConfig ,代码如下:
package com.szl.config;
import org.apache.dubbo.config.ApplicationConfig;
import org.apache.dubbo.config.ProtocolConfig;
import org.apache.dubbo.config.RegistryConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Dubbo配置类,以下几个类实际上都是 AbstractConfig类的子类,在Spring容器初始化这几个Bean的时候,
* 由于父类中的 AbstractConfig#addIntoConfigManager()方法是被@PostConstruct注解所修饰的,因此,
* 该方法会调用被自动调用。
* </p>
*/
@Configuration
public class DubboConfiguration {
@Bean
public ApplicationConfig applicationConfig() {
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName("service-provider");
return applicationConfig;
}
@Bean
public ProtocolConfig protocolConfig() {
ProtocolConfig protocolConfig = new ProtocolConfig();
protocolConfig.setPort(20880);
protocolConfig.setName("dubbo");
protocolConfig.setServer("netty4");
return protocolConfig;
}
@Bean
public RegistryConfig registryConfig() {
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress("nacos://127.0.0.1:8848");
return registryConfig;
}
@Bean
public ConfigCenterConfig configCenterConfig() {
ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();
configCenterConfig.setPort(8848);
configCenterConfig.setProtocol("nacos");
configCenterConfig.setAddress("127.0.0.1");
// 使用默认值
// configCenterConfig.setGroup("DEFAULT_GROUP");
// configCenterConfig.setConfigFile("application-service-provider.yml");
configCenterConfig.setNamespace("b1cb3cb8-9c6d-4cfc-a391-26aa0119a421");
return configCenterConfig;
}
}
我在注释中写的很详细,这四个配置类都有一个相同的父类,即AbstractConfig,并且在其父类中,有一个方法是被@PostConstruct注解修饰的。最后在初始化这个Bean的时候会,Spring会自动调用被这个注解修饰的方法,看看AbstractConfig#addIntoConfigManager()方法,代码如下:
在该方法中,调用了ApplicationModel.getConfigManager()方法,得到ConfigManager对象,即配置管理器,看看是怎么获取的,代码如下:
又需要看看ApplicationModel的LOADER属性是怎么获取的,其实就是看ExtensionLoader#getExtensionLoader(FrameworkExt.class),代码如下:
可知,ExtensionLoader有一个EXTENSION_LOADERS,用作缓存,如果通过传入的Class获取不到,则创建一个对象ExtensionLoader,调用其有参构造方法,传入Class,这里的Class就是FrameworkExt.class,并将创建好的ExtensionLoader放入EXTENSION_LOADERS,方便下次直接获取。具体看看ExtensionLoader的有参构造方法,代码如下:
因此又会调用到ExtensionLoader#getExtensionLoader()方法,也就是刚刚前面讲的。只不过此时的type属性为ExtensionFactory.class,然后还是先从EXTENSION_LOADERS中获取,假设是第一次,也获取不到,因此会调用ExtensionLoader的有参构造方法。再次回到上图的方法中,但是不同的是,这次由于type是等于ExtensionFactory.class,则此时objectFactory为null,并且还要调用ExtensionFactory#getAdaptiveExtension()方法,看看该方法,代码如下:
看看ExtensionFactory,代码如下:
ExtensionFactory确实被@SPI注解修饰了,只不过value值为"",因此ExtensionLoader#cacheDefaultExtensionName()方法中,最终设置的cachedDefaultName属性为null。回到ExtensionLoader#loadExtensionClasses(),紧接着就是调用ExtensionLoader#loadDirectory()方法,不过在此之前按,需要先看看ExtensionLoader的strategies属性,代码如下:
调用ServiceLoader#load()方法,传入LoadingStrategy.class,代码如下:
通过Java的stream流,遍历调用,new LoadingStrategy接口的实现类,得到对象,放入ExtensionLoader的strategies属性中,那LoadingStrategy接口的实现类是如何获取的呢?其实ServiceLoader是Java的 rt.jar下提供的类,属于Java自己实现的SPI机制,它会从classpath目录下的META-INF/services下,找一个名为LoadingStrategy接口全限定名的文件,即org.apache.dubbo.common.extension.LoadingStrategy,刚好找到了,代码如下:
因此最终实例化的就是这三个实现类,断点验证一下:
因此,我们如果想实现SPI机制,可以借助于Java自带的ServiceLoader实现,如果想实现更复杂的SPI机制,可以借鉴Dubbo,因为讲到这里,还不全是Dubbo的SPI机制。回到ExtensionLoader#loadExtensionClasses()方法,代码如下:
这里涉及到加载的路径,看看LoadingStrategy接口的三个实现类,代码如下:
可以知道,扫描的路径分别是:META-INF/dubbo/internal/ 或 META-INF/dubbo/ 或 META-INF/services/。ok,再看看ExtensionLoader#loadDirectory()方法,该方法是关键,代码如下:
可以知道,最终是根据前面的几个路径下,并且拼接接口的全限定名,得到文件路径,通过ClassLoader去找文件,得到urls并进行遍历,最终调用到ExtensionLoader#loadResource()方法,进行真正的加载,看看该方法,代码如下:
当然,现在看的是dubbo-common下的文件,在其他模块下,可能也存在org.apache.dubbo.common.extension.ExtensionFactory,这里就不去找了。再看看ExtensionLoader#loadClass()方法,代码如下:
顺便看看ExtensionFactory接口的实现类,如下:
由此可知,这两个实现类中,只有AdaptiveExtensionFactory是被@Adaptive注解修饰的。再回到ExtensionLoader#getAdaptiveExtensionClass()方法中,代码如下:
因此会返回AdaptiveExtensionFactory.class,再回到ExtensionLoader#createAdaptiveExtension()方法中,代码如下:
调用ExtensionLoader#injectExtension()方法,进行Dubbo的依赖注入,代码如下:
由于AdaptiveExtensionFactory中没有Setter方法,因此无需进行依赖注入,但是它的无参构造中,做了一些初始化处理,无参构造为:
需要看看ExtensionLoader#getSupportedExtensions()方法,代码如下:
这也是刚刚讲过的方法,最终只需要看ExtensionLoader#loadClass()方法,代码如下:
因此AdaptiveExtensionFactory不会被放入AdaptiveExtensionFactory的factories属性中,而是ExtensionFactory接口的其他实现,分别在两个文件中,如下:
打断点验证一下,结果如下:
这两个对象,即:SpringExtensionFactory和SpiExtensionFactory,将会是实现Dubbo的依赖注入的关键,看名字也能知道:前一个是通过Spring容器寻找依赖的对象;而后一个则是通过Dubbo的SPI机制寻找依赖的对象。当然,寻找依赖并不是直接通过这两个类来寻找的,而是通过AdaptiveExtensionFactory#getExtension()方法来寻找的,代码如下:
而SpringExtensionFactory的CONTEXTS属性,则是在ReferenceBean中或者ServiceBean总进行赋值的:
到这里为止,Dubbo的SPI机制大概讲清楚了,Dubbo的SPI剩余的内容,后面碰到再补充。想彻底搞懂这块,建议自己跟着源码,再配合博客一起看。OK,回到ApplicationModel#getConfigManager()方法传入的是 name,代码如下:
加载FrameworkExt接口实现类的逻辑跟ExtensionFactory类似,也是在classpath下,找文教名叫org.apache.dubbo.common.context.FrameworkExt的文件,如下:
最终得到这三个实现类的Class的集合,打断点结果如下:
回到ExtensionLoader#createExtension()方法中,代码如下:
通过name,即"config"可以获取到Class,即ConfigManager.class,然后通过EXTENSION_INSTANCES获取,由于是第一次,当然获取为空,因此进入if代码块,通过反射,实例化ConfigManager,并放入EXTENSION_INSTANCES中,其中key为"config",然后调用ExtensionLoader#injectExtension()方法,实际上就是调用AdaptiveExtensionFactory#getExtension()方法,寻找依赖,这块上文讲的很清楚了,这里不过多赘述。而ConfigManager有如下的Setter方法,代码如下:
这些Setter方法也不一定都会被调用,只有获取到了相应的对象,才会通过反射,进行赋值。需要说的是,给Setter方法赋值是不是一定通过Dubbo提供的依赖注入进行的赋值?这也不一定,可能在其他地方进行赋值,只不过Dubbo提供了这种方式。比如我打断点的时候,发现就不是通过Dubbo的方式进行的依赖注入,断点如下:
OK,至于是哪种方式调用Setter方法进行赋值的,这里不过多纠结,这并非核心流程。回到最开始的地方,即AbstractConfig#addIntoConfigManager()方法,由于该方法被@PostConstruct注解修饰,因此会自动被调用,断点如下:
在上篇博客中,讲到了往Spring容器中,添加了两个监听器,分别是:DubboBootstrapApplicationListener、DubboLifecycleComponentApplicationListener,这两个监听器会监听两种事件,分别是ContextRefreshedEvent、ContextClosedEvent。先看看DubboBootstrapApplicationListener,代码如下:
先看看DubboBootstrap#initialize()方法,代码如下:
在该方法中,又调用了七个方法,分别看看,代码如下:
① DubboBootstrap#initFrameworkExts()方法:
处理默认的配置中心,这里没有配置,因此均为空:
② DubboBootstrap#startConfigCenter()方法:
先看看我在DubboConfiguration配置的配置中心配置类ConfigCenterConfig,代码如下:
其中,group、configFile、namespace我都没有指定,将使用默认值,当然也可以种指定,默认值分别为:
然后我在Nacos上的配置为如下:
dubbo.properties 内容为:
目前我看的Dubbo版本应该是不支持yml/yaml的,继续往下看,重点看看DubboBootstrap#prepareEnvironment()方法,代码如下:
顺便提了下,为了实现配置中心的功能,我在pom文件中加了一个依赖:
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-configcenter-nacos</artifactId>
<version>2.7.10</version>
</dependency>
因此,我应该在该模块中找一个文件名为org.apache.dubbo.configcenter.support.nacos.NacosDynamicConfigurationFactory,也确实让我找到了,如下:
而它的名字也确实是nacos,符合预期,OK,回到代码继续往下看:
地址是怎么拼接的,也简单,就不带大家看了,感兴趣的自己看看。再回到DubboBootstrap#prepareEnvironment()方法,看看DynamicConfiguration#getProperties(),代码如下:
讲到这里就很清晰了,不必深究,因为后面我会专门写两篇博客,讲Nacos的注册中心源码和配置中心源码,敬请期待。再回到DubboBootstrap#prepareEnvironment()方法,打断点看看是否获取到了配置,如下:
结果符合预期,剩下的就是解析获取到的内容,也就是调用ConfigurationUtils#parseProperties()方法,代码如下:
最后调用Environment#updateExternalConfigurationMap()方法,将解析后的配置,即Map,设置到Environment的externalConfigurationMap属性中。
并且我没有看到把这些属性设置到Spring的环境变量对象中去的代码,因此,很可能这些配置只能从Dubbo的环境变量对象获取到,但是无法从Spring的环境变量对象获取到对应的属性值,这个也可以打断点验证一下:
确实如此,这是一个问题,还有一个问题是,如果我修改了dubbo.properties的内容,服务感知不到,需要自己实现(下文我做个示例,如何实现),这个就有点坑爹。当然我看的这个版本确实有些地方还不完善,后续版本肯定会做处理的,后面有空我再研究下Dubbo 3.X的版本,如果支持了,我在本篇博客中补充一下。至于感知Nacos配置中心是如何做到了,我也提一嘴,代码如下:
我在这里处理做个示例,代码如下:
修改Nacos配置,看看结果:
从修改到感知需要花4~5s的样子,反正获取的是全量的配置。然后可以直接调用之前讲的ConfigurationUtils#parseProperties()方法解析配置,再调用Environment#updateExternalConfigurationMap()方法,更新配置即可。
限于篇幅,剩下的内容将在下一篇博客中继续讲,敬请期待,如有错误,还望在留言中指正,感谢!下一篇博客地址为:《Dubbo源码深度解析(三)》