Dubbo内核的源码解析
Dubbo的SPI源码解析
下面以 Protocol 扩展实例的获取过程为例来解析 SPI 的执行过程。
分析思路
分析入口-ServiceConfig
private static final Protocol protocol = ExtensionLoader
. getExtensionLoader ( Protocol. class )
. getAdaptiveExtension ( ) ;
这里分解为两步:
获取扩展类的加载器:getExtensionLoader(Protocol.class) 获取自适应的扩展类:getAdaptiveExtension()
factory实例的loader创建
至此,ExtensionFactory 的 extensionLoader 实例就创建完毕。接下来调用 getAdaptiveExtension() 获取自适应的 extensionFactory。
获取自适应的 extensionFactory 实例
先整体分析下 getAdaptiveExtension() 这个方法:首先通过“双重检查锁”判断,调用 createAdaptiveExtension() 方法创建当前SPI的自适应实例。在创建自适应实例过程中,先通过 getAdaptiveExtensionClass() 方法获取到扩展类的类型,再通过反射调用无参构造器获取一个自适应实例,并完成实例注入(injectExtension
这个后面详细分析)。
获取扩展类类型
继续分析 getExtensionClasses() 方法:用于将当前SPI接口的所有扩展类class缓存起来(四类:普通扩展类、adaptive类、wrapper类,及activate类)
这里我们可以看出,Dubbo 对于具有多个功能性扩展名的扩展类,其采用了三种方式来缓存:
如果这个类是一个 Activate 类,其会专门缓存该类的第一个功能性扩展名到一个 map,这个 map 的 key 为这第一个扩展名,而 value 为 Activate 注解。 对于每一个扩展类,其都会将这个类与其第一个扩展名配对后存放到一个 map,这个 map 的 key 为这个扩展类,而 value 则为第一个扩展名。 对于每一个扩展类,其都会将每一个扩展名与这个类配对后放到一个map,这个 map 的 key 为扩展名,而 value 则为这个扩展类。
Debug分析下
获取ExtensionFactory实例
可以看到,dubbo 中有两类 ExtensionFactory:SpringExtensionFactory 和 SpiExtensionFactory 。由此可知,整个Dubbo框架中,所有的实例都是通过这两种方式创建出来的,一种是 spi 方式,一种是 spring 容器方式。如果 spi 方式的条件不满足,则采用 spring 容器的方式创建。
获取 Protocol 自适应实例
前面的代码执行完毕,然后一层层的返回,最终就返回到了这里:获取到了 Protocol 的 extensionLoader
此时,ExtensionFactory 的 ExtensionLoader 实例和 Protocol 的 ExtensionLoader 的 objectFactory 实例都创建完毕,返回其调用语句。
插件的IOC注入源码解析
下面以 Dubbo 的注册中心 Zookeeper 实例是如何创建的为例来解析IOC的过程。
找到IOC源码
继续分析:默认使用 zookeeper 注册中心作为配置中心
继续分析 getExtension() 方法:获取动态配置工厂 DynamicConfigurationFactory 的名称为 zookeeper 的扩展类实例
injectExtension()
接下来继续分析,调用instance实例的setter,完成实例的注入:
继续分析 getExtension() 方法:逐次尝试通过SPI与Spring两种方式创建扩展类实例
接下来再看下 Spring 方式获取扩展类实例的实现:其会分别根据名称、类型从Spring容器中获取实例
插件的AOP包装源码解析
Dubbo 的 AOP 是对 SPI 扩展类进行增强的方式,而 Wrapper 机制就是对 SPI 扩展类的增强。不同 SPI 的不同 Wrapper ,其增强的功能不同。 这里我们将之前 13-wrapper 工程的代码为例进行源码解析,来分析扩展类是如何被包装起来的。
找到AOP源码
运行示例代码,进行 Debug 分析:getExtension(extName) 首先获取到的是 TwoOrderWrapper 的实例(也可能是 OneOrderWrapper,wrapper 的缓存是放在 set 集合中的,无序)
调用执行
逐级返回,直至 Order@Adaptive 类。
继续 debug 分析完整个 Wrapper 的调用过程:
自动生成类的动态编译源码解析
当一个 SPI 接口 没有 Adaptive 类时,系统会根据 Adaptive 方法为其自动生成一个 Adaptive 类,这个自动生成的类是一个 java 代码类,这个类是需要编译的。而该编译是由系统动态完成的。
Javassist简介
Javassist 是一个开源的分析、编辑和创建 Java 字节码的类库。 一般情况下,对字节码文件进行修改是需要使用虚拟机指令的。而使用 Javassist 可以直接使用 java 编码的形式,而不需要了解虚拟机指令,就能动态改变类的结构,或者动态生成类。
手工实现 Javassist 动态编译 15-javassist
创建一个 Maven 工程,命名为 15-javassist,导入依赖:
< dependency>
< groupId> org.javassist</ groupId>
< artifactId> javassist</ artifactId>
< version> 3.26.0-GA</ version>
</ dependency>
public class JavassistCompiler {
public static void main ( String[ ] args) throws Exception {
ClassPool pool = ClassPool. getDefault ( ) ;
CtClass ctClass = genericClass ( pool) ;
invokeInstance ( ctClass) ;
}
private static CtClass genericClass ( ClassPool pool) throws Exception {
CtClass ctClass = pool. makeClass ( "com.yw.Person" ) ;
CtField nameField = new CtField ( pool. getCtClass ( "java.lang.String" ) , "name" , ctClass) ;
nameField. setModifiers ( Modifier. PRIVATE) ;
ctClass. addField ( nameField) ;
CtField ageField = new CtField ( pool. getCtClass ( "int" ) , "age" , ctClass) ;
ageField. setModifiers ( Modifier. PRIVATE) ;
ctClass. addField ( ageField) ;
ctClass. addMethod ( CtNewMethod. getter ( "getName" , nameField) ) ;
ctClass. addMethod ( CtNewMethod. getter ( "setName" , nameField) ) ;
ctClass. addMethod ( CtNewMethod. getter ( "getAge" , ageField) ) ;
ctClass. addMethod ( CtNewMethod. getter ( "setAge" , ageField) ) ;
CtConstructor ctConstructor = new CtConstructor ( new CtClass [ ] { } , ctClass) ;
String body = "{\nname=\"zhangsan\";\nage=23;\n}" ;
ctConstructor. setBody ( body) ;
ctClass. addConstructor ( ctConstructor) ;
CtMethod ctMethod = new CtMethod ( CtClass. voidType, "personInfo" , new CtClass [ ] { } , ctClass) ;
ctMethod. setModifiers ( Modifier. PUBLIC) ;
StringBuffer sb = new StringBuffer ( ) ;
sb. append ( "{\nSystem.out.println(\"name=\"+name);\n" )
. append ( "System.out.println(\"age=\"+age);\n}" ) ;
ctMethod. setBody ( sb. toString ( ) ) ;
ctClass. addMethod ( ctMethod) ;
byte [ ] bytes = ctClass. toBytecode ( ) ;
try ( FileOutputStream fos = new FileOutputStream ( new File ( "./Person.class" ) ) ) {
fos. write ( bytes) ;
}
return ctClass;
}
private static void invokeInstance ( CtClass ctClass) throws Exception {
Class< ? > clazz = ctClass. toClass ( ) ;
Object obj = clazz. newInstance ( ) ;
obj. getClass ( ) . getMethod ( "personInfo" , new Class [ ] { } ) . invoke ( obj, new Object [ ] { } ) ;
}
}
运行测试,控制台结果输出如下,并生成 Person.class 文件:
name= zhangsan
age= 23
解析Dubbo的动态编译
分析入口:ServiceConfig 中获取 Protocol 实例的自适应扩展类。
继续 Debug 看一下如何生成 Adaptive 类的代码的:
至此我们就知道了自适应扩展类的代码是如何动态生成的,接下来的工作就是对其进行动态编译,Debug 返回上一步,继续分析:获取默认扩展名的实例,即javassistCompiler的实例,并调用 javassistCompiler 的 compile() 方法
继续分析 javassistCompiler 的 compile() 方法:
接着我们看一下 doCompile 这个方法:使用的是 Javassist 技术完成的动态编译,至此就了解到了自适应扩展类的动态编译过程。
Dubbo业务框架的源码解析
Dubbo与Spring整合源码解析
查找解析器
从 dubbo 的 xml 配置文件开始:找到 dubbo 依赖,可以看到如下的三个文件,其中就包含 spring.schemas 和 spring.handlers 文件;打开 spring.schemas 和 spring.handlers 文件:DubboNamespaceHandler 就是 Dubbo 命名空间处理器。
Dubbo标签的解析
DubboNamespaceHandler 中有一个 init() 初始化方法,注册一个一个的 BeanDefinition 解析器,将配置文件中的各个标签通过 DubboBeanDefinitionParser 解析器进行解析,解析出来的结果封装到对应的 xxxConfig 对象中。
public class DubboNamespaceHandler extends NamespaceHandlerSupport {
static {
Version. checkDuplicate ( DubboNamespaceHandler. class ) ;
}
@Override
public void init ( ) {
registerBeanDefinitionParser ( "application" , new DubboBeanDefinitionParser ( ApplicationConfig. class , true ) ) ;
registerBeanDefinitionParser ( "module" , new DubboBeanDefinitionParser ( ModuleConfig. class , true ) ) ;
registerBeanDefinitionParser ( "registry" , new DubboBeanDefinitionParser ( RegistryConfig. class , true ) ) ;
registerBeanDefinitionParser ( "config-center" , new DubboBeanDefinitionParser ( ConfigCenterBean. class , true ) ) ;
registerBeanDefinitionParser ( "metadata-report" , new DubboBeanDefinitionParser ( MetadataReportConfig. class , true ) ) ;
registerBeanDefinitionParser ( "monitor" , new DubboBeanDefinitionParser ( MonitorConfig. class , true ) ) ;
registerBeanDefinitionParser ( "metrics" , new DubboBeanDefinitionParser ( MetricsConfig. class , true ) ) ;
registerBeanDefinitionParser ( "provider" , new DubboBeanDefinitionParser ( ProviderConfig. class , true ) ) ;
registerBeanDefinitionParser ( "consumer" , new DubboBeanDefinitionParser ( ConsumerConfig. class , true ) ) ;
registerBeanDefinitionParser ( "protocol" , new DubboBeanDefinitionParser ( ProtocolConfig. class , true ) ) ;
registerBeanDefinitionParser ( "service" , new DubboBeanDefinitionParser ( ServiceBean. class , true ) ) ;
registerBeanDefinitionParser ( "reference" , new DubboBeanDefinitionParser ( ReferenceBean. class , false ) ) ;
registerBeanDefinitionParser ( "annotation" , new AnnotationBeanDefinitionParser ( ) ) ;
}
}
DubboBeanDefinitionParser:
public class DubboBeanDefinitionParser implements BeanDefinitionParser {
public DubboBeanDefinitionParser ( Class< ? > beanClass, boolean required) {
this . beanClass = beanClass;
this . required = required;
}
@SuppressWarnings ( "unchecked" )
private static BeanDefinition parse ( Element element, ParserContext parserContext, Class< ? > beanClass, boolean required) {
}
}
接下来就简单分析下 DubboBeanDefinitionParser 的 parse() 方法:
step1-创建并初始化解析对象
创建 RootBeanDefinition 对象,并设置不进行延迟初始化
step2-解决id问题
解决 id 为空和 id 重复的问题:
若标签中 id 属性值为空,且必须,则获取 beanName 值作为 id 值;若 id 值重复,则从 2 开始一次加1; beanName 值首先从 name 属性值获取,若 name 属性值为空,且标签为 <dubbo:protocol>,则 name 属性值设为 dubbo,否则取 interface 属性值;若 beanName 仍为空,则去 beanClass 的类名,这样一定能保证 beanName 不为空,即 id 不为空。
step3-将id属性写入到解析对象
id 属性不为空时,又进行了一次判重,主要防止用户自定义id重复。然后将 id 与 BeanDefinition 注册到解析上下文,并将 id 属性添加到 BeanDefinition 中。
step4-对特殊标签的特殊处理
主要特殊处理了四类标签:<dubbo:protocol/>、<dubbo:service/>、<dubbo:provider/>、<dubbo:consumer/>,以及 嵌套解析<dubbo:servie/>与<dubbo:provider/>标签,<dubbo:reference/>与<dubbo:consumer/>标签。
step5-对所有标签的普适性处理
重要接口
Invocation
Invocation 封装了远程调用的具体信息:Invoker 服务提供者、方法名、参数与参数类型、附件
Invoker
Invoker 是提供者 provider 的代理对象,在代码中就代表提供者。特别是在消费者进行远程调用时,其通过服务路由、负载均衡、集群容错等机制要查找的就是 Invoker。找到了其需要的 Invoker 实例就可以进行远程调用了。
Exporter
Exporter 是服务暴露对象。其包含一个很重要的方法 getInvoker(),用于获取当前服务暴露实例所包含的远程调用实例 Invoker,即可以进行的远程调用。而 unexport()方法会使服务不进行服务暴露。
Directory
Directory 中包含一个很重要的方法 list(),其返回结果为一个 List<Invoker>。其实简单来说,可以将 Directory 理解为一个动态的 Invoker 列表。Directory 也继承自 Node,因此也可以作为 adaptive 方法的参数。
服务发布
分析入口
DubboNamespaceHandler 命名空间处理器的初始化方法:从 ServiceBean 这个类开始分析,ServiceBean 中实现了很多 Spring 的接口,其中有一个 ApplicationListener 接口,这个接口只有一个方法 onApplicationEvent(event),当Spring容器被刷新时会触发该方法的执行。
registerBeanDefinitionParser ( "service" , new DubboBeanDefinitionParser ( ServiceBean. class , true ) ) ;
正式进行服务暴露之前,做了一些校验工作:
检测注册中心的可用性,不可用则直接抛异常结束 判断是否需要进行服务暴露,已经是否延迟服务暴露
分析 doExport() 方法
首先判断是否已经取消发布,或者已经发布过了。然后将注册中心与服务暴露协议相结合进行服务暴露:获取所有注册中心的标准化URL,遍历当前服务所支持的所有服务暴露协议,并与每一个注册中心形成一个 invoker 进行服务暴露。
获取所有注册中心的标准化URL
详细分析一下 loadRegistries 这个方法:解析 <dubbo:registry/> 标签,封装了很多属性到一个 Map 集合中,并将所有注册中心地址都进行 URL 标准化。
分析一下 appendRuntimeParameters(map) 方法:将运行时的一些参数值写入到map
分析 parseURLs() 方法:获取当前的address属性,并解析出的所有注册中心URL
为每一个协议暴露到所有注册中心
首先,判断协议名称是否为空,若为空,则使用 dubbo 协议进行暴露。然后,创建一个 Map 用于存放各种属性,这些属性将来要封装到 URL 的元数据中。
形成服务暴露 URL,并且可以对现有的扩展类进行再配置(通过定义ConfiguratorFactory SPI接口的实现类),完成对 URL 的再配置。这又是 Dubbo 扩展性的体现。
下面继续分析 doExportUrlsFor1Protocol() 这个方法后面的代码:首先获取 scope 属性值,scope 属性值不为 none,才进行服务暴露。并且当 scope 属性值不为remote,则进行本地暴露;当 scope 属性值不为local,则进行远程暴露。这意味着 scope 如果随便配置一个值,例如 scope=“xxx”,则会本地和远程都会进行服务暴露。 在进行远程服务暴露时,会根据有没有注册中心,生成不同的 invoker 代理对象,最后完成服务暴露,并将 exporter 添加到 List 集合中。 分析到这里,我们就已经找到了 本地暴露和远程暴露 分析的入口,同时也找到了动态生成提供者代理对象 分析的入口,这些在后面会进行详细分析。
泛化服务/泛化引用
在<dubbo:service/> 与 <dubbo:reference> 中都有一个 generic 属性,其在 <dubbo:service/> 标签中可以提供泛化服务,在 <dubbo:reference> 中可以提供泛化引用。泛化服务与泛化引用无需同时使用。其主要是针对某一方没有具体业务接口的 .class 情况的。这又是 Dubbo 扩展性的一个体现。
提供者工程 16-provider-generic
复制 02-provider-zk 工程,重命名为 16-provider-generic,在此基础上修改。去掉 00-api 工程的依赖,即不再需要该业务接口了。 定义 GerericServiceImpl 类:
public class GenericServiceImpl implements GenericService {
@Override
public Object $invoke ( String method, String[ ] parameterTypes, Object[ ] args) throws GenericException {
if ( "hello" . equals ( method) ) {
return "Generic hello, " + args[ 0 ] ;
}
return null;
}
}
消费者工程 16-consumer-generic
复制 02-consumer-zk 工程,重命名为 16-consumer-generic,在此基础上修改。去掉 00-api 工程的依赖,即不再需要该业务接口了。 修改 spring-consumer.xml:
测试:启动 provider 和 consumer:
进行本地服务暴露
首先修改 URL 的协议、主机和端口号,然后调用 InjvmProtocol 的 export() 方法,创建 InjvmExporter 对象完成服务暴露,即存放到 Map 集合,key 是服务接口名,value 是 InjvmExporter 对象。
进行远程服务暴露
分析入口:在 ServiceConfig 的 doExportUrlsFor1Protocol 方法中
Exporter< ? > exporter = protocol. export ( wrapperInvoker) ;
接下来一步步 Debug 跟踪 export 方法的调用:这里是注册中心的一系列export ,依次经过 Protocol$Adaptive、ProtocolListenerWrapper、ProtocolFilterWrapper、RegistryProtocol
接下来整体先分析一下 RegistryProtocol 的 export() 这个方法:
获取注册中心URL和提供者URL
获取注册中心URL:通过源 invoker 提取出元数据 registry=zookeeper,并将 registry 替换为 zookeeper,这样就拿到了指定协议的注册中心 URL,例如:
zookeeper://192.168.254.120:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&export=dubbo%3A%2F%2F192.168.0.100%3A20880%2Forg.apache.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26bean.name%3Dorg.apache.dubbo.demo.DemoService%26bind.ip%3D192.168.0.100%26bind.port%3D20880%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.DemoService%26methods%3DsayHello%26pid%3D25811%26qos.port%3D22222%26register%3Dtrue%26release%3D%26side%3Dprovider%26timestamp%3D1624979190366&pid=25811&qos.port=22222×tamp=1624979190354
获取提供者URL:也是通过源 invoker 提取出元数据 export 的值,并进行解码得到提供者URL,例如:
dubbo://192.168.0.100:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.0.100&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello&pid=25811&qos.port=22222®ister=true&release=&side=provider×tamp=1624979190366
获取注册中心实例
先跳过 doLocalExport(originInvoker, providerUrl)
这个方法,即在完成服务暴露之后,分析下如何获取到注册中心实例。getRegistry 这个方法依次经过:RegistryProtocol、RegistryFactory$Adaptive、AbstractRegistryFactory,在 AbstractRegistryFactory 中使用 ReentrantLock 加锁进行创建,并将创建出的注册中心实例缓存到Map中。
继续分析 createRegistry 这个方法:通过 ZK 注册中心工厂类创建 ZK 注册中心,创建 ZK 客户端时,使用的是 Dubbo 自己经过一定封装的 Curator,连接失败会在重试范围内进行重连,连接的URL:
zookeeper://192.168.254.120:2181/org.apache.dubbo.registry.RegistryService?application=demo-provider&dubbo=2.0.2&interface=org.apache.dubbo.registry.RegistryService&pid=28537&qos.port=22222×tamp=1624981736384
完成真正的服务暴露
现在来分析 doLocalExport 这个方法,即完成正在的服务暴露:这里是dubbo服务的一系列export ,依次进过 RegistryProtocol、Protocol$Adaptive、ProtocolFilterWrapper、ProtocolListenerWrapper、DubboProtocol
分析 DubboProtocol 的 export 方法
主要完成两件事:
创建一个与服务暴露协议相绑定的 exporter 对象,并写入到缓存 map 中; 创建并启动一个 Netty Server
分析 openServer 方法
会创建出一个 ExchangeServer(十层架构里面的同异步转换服务对象),并缓存到 map 中,一个 ExchangeServer 仅会负载一个 NettyServer 的同异步转换工作,即一个 ExchangeServer 就会对应一个 NettyServer
继续分析一堆 bind 方法:依次进过 Exchangers、HanderExchanger、Transporters、Transporter$Adaptive、NettyTransporter
继续跟踪 doOpen() 方法:终于找到 NettyServer 创建的地方了。
重置 server(更新url)
前面创建 ExchangeServer 时使用了双重检查锁,是为了保证同一个服务暴露协议,在同时暴露当前主机中的多个服务时,只会创建出一个 ExchangeServer。那么当第一个服务完成 ExchangeServer 创建后,后面的服务再进行服务暴露时,需要重置 server,即更新url的值,例如第一个服务暴露后的url:
dubbo://192.168.0.100:20880/org.apache.dubbo.demo.DemoService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.DemoService&bind.ip=192.168.0.100&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.DemoService&methods=sayHello,sayHi&pid=34143&qos.port=22222®ister=true&release=&side=provider×tamp=1624986940711
重置URL:即将后暴露的URL数据写入到之前暴露的URL中,重置后的 url 变为了:
dubbo://192.168.0.100:20880/org.apache.dubbo.demo.SomeService?anyhost=true&application=demo-provider&bean.name=org.apache.dubbo.demo.SomeService&bind.ip=192.168.0.100&bind.port=20880&deprecated=false&dubbo=2.0.2&dynamic=true&generic=false&interface=org.apache.dubbo.demo.SomeService&methods=sayGood,sayHi&pid=33760&qos.port=22222®ister=true&release=&side=provider×tamp=1624986628596
完成服务注册
现在再来详细分析一下如何将提供者注册到 ZK 中,即前面分析到的 RegistryProtocol 中的 register 这个方法,register 方法依次进过 RegistryProtocol、FailbackRegistry、AbstractRegistry、FailbackRegistry、ZookeeperRegistry、AbstractZookeeperClient。 从缓存中获取注册中心实例,将提供者 URL 发送到注册中心ZK,完成服务注册,即写入临时节点,节点信息例如:
/dubbo/org.apache.dubbo.demo.DemoService/providers/dubbo%3A%2F%2F192.168.0.100%3A20880%2Forg.apache.dubbo.demo.DemoService%3Fanyhost%3Dtrue%26application%3Ddemo-provider%26bean.name%3Dorg.apache.dubbo.demo.DemoService%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dorg.apache.dubbo.demo.DemoService%26methods%3DsayHello%2CsayHi%26pid%3D35309%26register%3Dtrue%26release%3D%26side%3Dprovider%26timestamp%3D1625016941968