Dubbo 源码分析

admin 2022年3月29日03:14:01评论41 views字数 11857阅读39分31秒阅读模式

最近准备对Dubbo的历史漏洞进行分析,但觉得不懂Dubbo的设计和实现直接去分析漏洞比较困难,所以在分析漏洞前先分析Dubbo的源码及实现,值得一提的是Dubbo的官网也有非常详细的源码分析的过程。

SPI机制及实现

Dubbo的SPI是对JDK自身SPI的扩展实现,增加了IOC和AOP的功能,是Dubbo实现的核心,Dubbo SPI需要的配置文件放在/meta- inf/dubbo目录下,通过键值对的方式配置,如下所示:

adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory

Dubbo的SPI和JDK自身的SPI对比如下,这也是Dubbo没有选择使用JDK自带SPI的原因。
Dubbo 源码分析
可以通过@SPI注解将接口声明由Dubbo的SPI机制加载实现类。
Dubbo 源码分析

Dubbo如何实现SPI?

ExtensionLoader是Dubbo
SPI实现的核心类,每个定义的spi的接口都会构建一个ExtensionLoader实例。一般通过ExtensionLoader.getExtensionLoader获取ExtensionLoader实例。
getExtensionLoader首先判断是否为接口类型并且由@SPI注解修饰,也就是说只有@SPI修饰的接口才会由Dubbo的SPI机制去寻找实现类。下面会通过EXTENSION_LOADERS寻找是否已经有loader的实例,没有的话会创建一个并添加到EXTENSION_LOADERS中。
Dubbo 源码分析
下面分析ExtensionLoader构造方法,如果type类型不为ExtensionFactory则先执行ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()
Dubbo 源码分析
getAdaptiveExtension首先从缓存中获取实例,没有则通过createAdaptiveExtension创建实例。
Dubbo 源码分析
createAdaptiveExtension首先通过getAdaptiveExtensionClass().newInstance()创建实例,再通过injectExtension包装。
Dubbo 源码分析
getAdaptiveExtensionClass首先通过getExtensionClasses获取Class,找不到则通过createAdaptiveExtensionClass创建。
Dubbo 源码分析
getExtensionClasses首先通过缓存获取Class获取不到则通过loadExtensionClasses方法获取,获取后放到classesMap中。
Dubbo 源码分析
loadExtensionClasses首先获取SPI注解的属性值放到缓存中,下面通过loadDirectory从配置文件中加载Class,主要从META- INF/dubbo/META-INF/services/META-INF/dubbo/internal几个目录下加载文件。
Dubbo 源码分析
Dubbo 源码分析
根据dir和type作为文件名加载资源,并通过loadResource加载类的信息并放到extensionClasses中。
Dubbo 源码分析
loadResource中读取文件并解析文件内容获取name接口实现类的名称,下面通过loadClass加载。
Dubbo 源码分析
loadClass中首先检查clazz是否是type的实现类,再去检测clazz的接口是否有Adaptive注解存在的话放到将类放到cachedAdaptiveClass缓存中,下面再通过是否有参数为clazz的构造方法,有的话将clazz存到cachedWrapperClasses中,下面查看实现类是否有Extension注解,有的话取出这个注解的值并赋值给name。下面获取name的值,可以通过xxx,xxx,xx=xxx.com等形式传入多个name,并通过saveInExtensionClassnameclass的值保存到extensionClasses中。Dubbo 源码分析
下面在回到getAdaptiveExtensionClass方法中,首先在缓存中查找,找不到则会通过createAdaptiveExtensionClass创建Class。
Dubbo 源码分析
createAdaptiveExtensionClass首先根据typeSPI配置的value的值生成Adaptive包装类并编译为Class,也就是说我们获取的类型不是配置的实现类对象,而是Adaptive包装类对象。Dubbo 源码分析
生成代码的逻辑比较复杂,我们就不深入分析了,不过我们可以拿到生成的代码,可以看看生成的代码主要做了什么。首先它是type接口的实现类,如果接口中的方法没有通过Adaptive修饰,则直接抛出异常。
Dubbo 源码分析
对于Adaptive注解修饰的方法则会生成实现,首先检查Invoker的url是否为空,再获取协议信息,如果没有配置协议则默认使用dubbo协议,下面获取Protocol的实现类并执行实现类的export方法,其实也就是对export进行了一些包装,在执行export前加了一些验证逻辑。
Dubbo 源码分析
refer方法逻辑类似,只是最后调用实现类的refer方法。
Dubbo 源码分析
下面我们再回到createAdaptiveExtension方法中,通过getAdaptiveExtensionClass()已经拿到了动态创建的Adaptive类并通过newInstance创建对象,下面通过injectExtension完成依赖注入。Dubbo 源码分析

如何实现IOC?

injectExtension获取setter方法,并通过objectFactory.getExtension(pt, property);获取需要注入的对象,通过反射调用setter方法完成依赖注入。
Dubbo 源码分析
objectFactory可能是下面三种实现类,也就是说除了可以通过Spi获取注入的对象也可以从spring中获取注入对象。而AdaptiveExtensionFactory则会循环调用多个factory获取对象。
Dubbo 源码分析
一般objectFactory经过初始化后会封装为AdaptiveExtensionFactory并且包含了spispring两个工厂,也就是说默认会通过spispring两种方式加载需要注入的对象。
Dubbo 源码分析

为什么可以得到AdaptiveExtentionFactory?

在容器启动时,会解析<dubbo:service对象,并创建一个serviceBean实例,这个实例是serviceConfig的子类,创建serviceBean实例的过程中也会执行父类的static属性,会执行如下操作。
Dubbo 源码分析
跳过一些已经分析过的步骤,在执行ExtensionLoader构造方法时,会判断类型是否为ExtensionFactory类型,如果不是则会执行后面的代码。
Dubbo 源码分析
进入getExtensionLoader方法,如果缓存中没有extensionLoader则重新创建一个,也就是说这里还会再调用一次ExtensionLoader的构造方法。
Dubbo 源码分析
由于这次type的类型为ExtensionFactory,所以会返回一个ExtensionLoader对象但是此时objectFactory属性为空。
Dubbo 源码分析
下面通过getAdaptiveExtesion获取ExtensionFactory的实现类,同样中间的过程不分析了,主要关注在loadExtensionClasses中获取了ExtensionFactory的实现类。
Dubbo 源码分析
但是ExtensionFactory中明明配置了三个实现类,为什么加载后变成了两个而没有AdaptiveExtensionFactory
Dubbo 源码分析
我们跟进资源加载部分的代码,可以看到确实读取到了AdaptiveExtensionFactory
Dubbo 源码分析
loadClass中,由于AdaptiveExtensionFactory实现类由Adaptive注解修饰,因此会该类到缓存cachedAdaptiveClass中并返回,并不会执行后面的saveInExtensionClass方法。
Dubbo 源码分析
在执行完getExtensionClasses后,会判断cachedAdaptiveClass中是否有值,有的话则直接返回,所以这里其实通过getAdaptiveExtensionClass返回了AdaptiveExtensionFactory类。
Dubbo 源码分析
所以下面是创建了AdaptiveExtensionFactory的实例。
Dubbo 源码分析
而在AdaptiveExtensionFactory的构造方法中,通过loader.getSupportedExtensions()获取扩展名,并通过loader.getExtension("spring")获取对应的工厂封装到factorties中。
Dubbo 源码分析
Dubbo 源码分析
除了动态生成ProtocolAdaptive包装类外,还生成了proxyFactoryAdaptive包装类。
Dubbo 源码分析
在Dubbo中主要使用了两种代理方式,即JDK和javassist。
Dubbo 源码分析
proxyFactoryAdaptive中主要实现了getProxygetInvoker方法,如果没有配置代理则默认使用javaassist动态代理。
Dubbo 源码分析
getInvoker中则根据传入的参数完成方法的调用。
Dubbo 源码分析

标签解析过程

上面分析了Dubbo SPI机制的实现过程,下面分析下Dubbo 中配置的标签是如何解析的?
NamespaceHandler用来解析Spring遇到的所有这个特定的namespace配置文件中的所有elements,Dubbo
实现了DubboNamespaceHandler作为Dubbo标签中属性的处理器,在它的init方法中,配置了不同element的标签解析器。
Dubbo 源码分析
并且Dubbo扩展了BeanDefinitionParser,自定义了DubboBeanDefinitionParser将标签中配置的属性值设置到Bean中,会对bean的属性赋值。
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析
Dubbo 源码分析

服务导出过程

通过DubboNamespaceHandler中的配置,可以知道service元素的配置信息会被方法ServiceBean中。
Dubbo 源码分析
ServiceBean中实现了ApplicationListener监听器接口,每当ApplicationContext发布ApplicationEvent时,实现ApplicationListener的Bean将自动被触发。
Dubbo 源码分析
所以会触发ServiceBean.onApplicationEvent方法。
Dubbo 源码分析
ServiceBean.onApplicationEvent中通过export方法导出服务。
Dubbo 源码分析
ServiceBean#export中,调用父类ServiceConfig.export,首先判断做了一些检查,检测是否导出,和延时导出后通过doExport完成导出。
Dubbo 源码分析
doExport中首先判断是否已经导出过了,再判断是否设置path如果没有则将interfaceName作为path并调用doExportUrls方法。
Dubbo 源码分析
doExportUrls首先通过loadRegistries加载注册中心的地址,其次通过buildKey获取接口名,将接口名、实现类实例、接口Class封装到ProviderModel中。再通过initProviderModel将serviceName和providerModel
放到providedServices中。最后通过doExportUrlsFor1Protocol导出服务。
Dubbo 源码分析
Dubbo 源码分析
doExportUrlsFor1Protocol首先获取name属性值,为空则默认name为dubbo。创建一个存放参数的map,将一些配置的参数放到map中。
Dubbo 源码分析
下面通过接口Class构造了接口的包装类,通过包装类获取所有的method,并将methed添加到map中。
Dubbo 源码分析
Dubbo 源码分析
下面获取host和port,并通过这些信息和map中的信息构造一个org.apache.dubbo.common.URL对象,其中path为interfaceName,map中保存的信息被当作参数。
Dubbo 源码分析
Dubbo 源码分析
当scope属性没有配置时,则默认先通过exportLocal先发布到本地,再发布到远程。
Dubbo 源码分析

本地导出

本地导出主要在exportLocal中实现,首先判断协议名是否为injvm,如果是则表示已经导出过了,不再进行导出。下面构建本地导出的url,获取Invoker并导出。
Dubbo 源码分析
这里的proxyFacory为之前分析SPI机制时动态生成的ProxyFactoryAdaptive类,它的getInvoker方法如下,默认情况下会使用javasist代理。
Dubbo 源码分析
JavassistProxyFactory#getInvoker首先创建了实现类的包装类,再创建了AbstractProxyInvoker对象并重写了doInvoke方法
Dubbo 源码分析
protocol也是在SPI机制动态生成的Adaptor,其export方法如下,
Dubbo 源码分析
而具体调用哪个Protocol的export方法是由(org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName)的返回结果决定的,如果我们配置的是是injvm协议,则返回injvmProtocol的包装对象。
Dubbo 源码分析
ProtocolFilterWrapper#export中,首先判断是否是registry协议,如果是则直接导出否则通过buildInvokerChain构建过滤器链后再导出。
Dubbo 源码分析
buildInvokerChain构造过滤器链,只有当左右的Filter执行完后才会执行invoker的invoke方法。
Dubbo 源码分析
InjvmProtocol#export创建InjvmExporter对象完成本地导出。
Dubbo 源码分析远程导出

远程导出首先还是获取Invoker,再将Invoker和serviceBean封装到DelegateProviderMetaDataInvoker中,最后调用export方法导出。

Dubbo 源码分析

服务导出

下面分析RegistryProtocol#export,在export方法中,主要通过RegistryProtocol#doLocalExport完成服务导出。首先从Invoker中获取key,其次创建了InvokerDelegate作为Invoker的委托类,最终通过protocol.export完成导出
Dubbo 源码分析
由于我这里使用的是http协议,但HttpProtocol没有export方法,因此会调用父类AbstractProxyProtocol的export方法。在AbstractProxyProtocol#export中,首先判断是否已经导出过,如果没有则通过doExport完成导出。
Dubbo 源码分析
HttpProtocol#doExport中,首先互获取绑定地址,从serverMap缓存中获取server,没有的话通过bind创建server。
Dubbo 源码分析
bind的同时创建了InternalHandler,其中handle方法内容为当通过post请求时,会通过HttpInvokerServiceExporter.handleRequest处理请求。
Dubbo 源码分析
httpBinder也是Adaptive类,其内容如下,在export方法中,从url中获取server属性并根据server属性得到HttpBinder的实现类,并调用实现类的bind方法。如果没有配置server属性则默认为jetty。

public class HttpBinder$Adaptive implements org.apache.dubbo.remoting.http.HttpBinder {
public org.apache.dubbo.remoting.http.HttpServer bind(org.apache.dubbo.common.URL arg0, org.apache.dubbo.remoting.http.HttpHandler arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("server", "jetty");
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.remoting.http.HttpBinder) name from url (" + url.toString() + ") use keys([server])");
org.apache.dubbo.remoting.http.HttpBinder extension = (org.apache.dubbo.remoting.http.HttpBinder)ExtensionLoader.getExtensionLoader(org.apache.dubbo.remoting.http.HttpBinder.class).getExtension(extName);
return extension.bind(arg0, arg1);
}
}

由于我们配置的server为tomcat因此会调用TomcatHttpBinder#bind方法,创建一个TomcatHttpServer.
Dubbo 源码分析
下面构造Tomcat服务需要的一些参数,并且动态创建一个servlet,启动tomcat服务器。
Dubbo 源码分析
启动server后,获取path,并将path作为key,通过createExporter创建的HttpInvokerServiceExporter作为值put到skeletonMap中。
Dubbo 源码分析
HttpInvokerServiceExporter添加接口信息和实现类.
Dubbo 源码分析
最后创建一个Runnable对象并返回。
Dubbo 源码分析
回到AbstractProxyProtocol#export中,得到runnable对象后,创建AbstractExporter对象并返回,将exporter放到缓存中后返回。
Dubbo 源码分析

服务注册

首先获取registry实例,并且获取providerUrl,通过registerProvider将provider注册到providerInvokers中。
Dubbo 源码分析
Dubbo 源码分析
下面调用register方法进行服务注册。
Dubbo 源码分析
register方法中主要通过doRegister完成注册。
Dubbo 源码分析
由于我们使用的是zookeeper作为注册中心,所以会通过ZookeeperRegistry#doRegister完成服务注册。
Dubbo 源码分析

服务引用过程

服务引用相当于生成了一个代理类,这个代理类可以给我们屏蔽远程调用的细节。
服务引用分为三种,即本地引用,远程引用和注册中心引用。
下面介绍来自字节面试:dubbo的服务引用过程

本地引入不知道大家是否还有印象,之前服务暴露的流程每个服务都会通过搞一个本地暴露,走 injvm 协议(当然你要是 scope = remote 就没本地引用了),因为存在一个服务端既是 Provider 又是 Consumer 的情况,然后有可能自己会调用自己的服务,因此就弄了一个本地引入,这样就避免了远程网络调用的开销。
直连远程引入服务,这个其实就是平日测试的情况下用用,不需要启动注册中心,由 Consumer 直接配置写死 Provider 的地址,然后直连即可。
注册中心引入远程服务,这个就是重点了,Consumer 通过注册中心得知 Provider 的相关信息,然后进行服务的引入

服务引用主要通过ReferenceBean来实现,这个类实现了FactoryBean接口,在spring容器初始化时会调用ReferenceBean#getObject方法。
Dubbo 源码分析
get先调用checkAndUpdateSubConfigs检查属性值是否正确,再调用init完成服务引用。
Dubbo 源码分析
init方法首先将很多信息封装到map中,再调用createProxy创建代理。
Dubbo 源码分析
判断是否为本地调用,如果为本地调用,则调用refprotocol.refer创建一个InjvmInvoker对象返回。
Dubbo 源码分析
判断url是否为空,不为空则判断是远程引用还是注册中心引用。
Dubbo 源码分析
获取注册中心的地址,判断是否配置监控中心,如果没有则跳过,最后向url中加入refer参数。
Dubbo 源码分析
下面当url只有一个时则直接调用prtocol.refer生成invoker。如果有多个url则取最后一个registry的url作为registryURL,获取多个invoker添加到invokers中,并将invokers封装到StaticDirectory中,通过cluster封装为一个invoker,而这个invoker的地址为registryURL。
Dubbo 源码分析
再看下refprotocol.refer是如何做的,由于我们使用的是registry协议,所以会调用RegistryProtocol#refer,首先获取url中的registry参数,并将参数的内容设置为url的协议名重新构造url,其次获取registry实例,如果要调用的接口名是RegistryService的实例,则直接构造Invoker返回,最后对group参数做处理,如果没有则直接调用doRefer完成核心的服务引用的逻辑。
Dubbo 源码分析
doRefer首先创建了RegistryDirectory对象,RegistryDirectory 是一种动态服务目录,实现了
NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory
可收到与当前服务相关的变化。接下来构造consumer的url并注册到注册中心,通过subscribe订阅provider和router等服务,订阅了之后
RegistryDirectory
会收到这几个节点下的信息,触发Invoker的生成。最后通过cluster封装directory得到Invoker,将Consumer的信息添加到ProviderConsumerRegTable后返回Invoker。
Dubbo 源码分析
现在主要关注subscribe订阅方法,订阅过程中会调用对应协议的refer方法,由于我们配置的是http协议,但HttpProtocol没有实现refer方法,因此会调用父类AbstractProxyProtocol.refer
Dubbo 源码分析
AbstractProxyProtocol#refer首先调用HttpInvoker.doRefer获取http调用客户端代理类对象,并通过getInvoker将代理类转换为Invoker,创建AbstractInvoker对象并实现doInvoke方法,在doInvoke中调用invoker.invoke方法,完成服务调用并获取返回结果。
Dubbo 源码分析
HttpInvoker.doRefer创建了HttpInvokerProxyFactoryBean
在spring中提供了HttpInvoker 通过HTTP协议实现RPC调用,Spring
HttpInvoker使用Java序列化来序列化参数和返回值,然后基于HTTP协议传输经序列化后的对象。Spring HttpInvoker的服务端和客户端分别由HttpInvokerServiceExporterHttpInvokerProxyFactoryBean实现。
服务端处理如下:
Dubbo 源码分析
客户端处理如下:
Dubbo 源码分析
创建HttpInvokerProxyFactoryBean对象后重写了createRemoteInvocation方法,根据不同的dubbo版本创建的不同的RemoteInvocation对象。
Dubbo 源码分析
下面设置url和intercepte属性,并且创建了发送请求的客户端并封装到httpProxyFactoryBean中。创建SimpleHttpInvokerRequestExcutor对象并设置到httpProxyFactoryBean中。
Dubbo 源码分析
下面调用afterPropertiesSet方法,创建ProxyFactory对象,通过getProxy获取AOP代理对象。
Dubbo 源码分析
这里传入的interceptor是HttpInvokerProxyFactoryBean,这个Bean的父类HttpInvokerClientInterceptor继承了MethodInterceptor,因此这里相当于添加了一个环绕通知。
Dubbo 源码分析
最后调用getObject实际上是返回HttpProxyFactoryBean的代理对象。
Dubbo 源码分析

服务调用过程

服务调用是通过消费者的代理对象发起的,这个代理对象中包含了之前创建的服务引用。
Dubbo 源码分析
查看org.apache.dubbo.rpc.proxy.InvokerInvocationHandler#invoke,封装调用的方法名和参数到RpcInvocation中,调用MockClusterInvoker.invoke
Dubbo 源码分析
org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#invoke首先判断是否使用Mock机制,如果没有则调用AbstractClusterInvoker.invoke
Dubbo 源码分析
AbstractClusterInvoker.invoke得到Invoker,初始化负载均衡调用FailoverClusterInvoker.doInvoke
Dubbo 源码分析
FailoverClusterInvoker.doInvoke利用负载均衡策略选择一个invoker,并通过RpcContext记录调用过的Invoker,最后执行invoker的invoke方法。
Dubbo 源码分析
回想一下服务引用的过程,真正执行请求的Invoker被封装为AbstractInvoker,所以会调用AbstractInvoker.invoke方法,设置invocation的invoker,并调用doInvoke方法。
Dubbo 源码分析
我们实现的AbstractProxyProtocol重写了doInvoke方法,执行代理类的invoke方法。
Dubbo 源码分析
这个代理类是AbstractProxyInvoker的实例,因此会调用AbstractProxyInvoker.invoke
Dubbo 源码分析
AbstractProxyInvoker.invoke调用了重写的doInvoke方法,也就是通过wapper.invokeMethod调用。也就是调用proxy的methodname方法。
Dubbo 源码分析
由于我们添加了环绕通知,因此会调用HttpInvokerClientInterceptor.invoke执行调用请求。
Dubbo 源码分析
HttpInvokerClientInterceptor#executeRequest中获取executer并执行executeRequest方法。
Dubbo 源码分析
由于我们之前服务引用在HttpInvokerProxyFactoryBean中设置的是SimpleHttpInvokerRequestExecutor,但SimpleHttpInvokerRequestExecutor中没有executeRequest,因此调用父类AbstractHttpInvokerRequestExecutor.executeRequest,在这个类中,将RemoteInvocation进行序列化后调用SimpleHttpInvokerRequestExecutor#doExecuteRequest完成请求发送。
Dubbo 源码分析
Dubbo 源码分析
之前在服务导出时我们已经开启了tomcat服务并且创建了internalHandler设置到DispatcherServlet中,当接收到请求时,将通过internalHandler.handle处理请求。
Dubbo 源码分析
获取HttpInvokerServiceExporter处理request请求。
Dubbo 源码分析
通过readRemoteInvocation将传入的数据反序列化为RemoteInvocation对象,调用invokeAndCreateResult完成请求处理。
Dubbo 源码分析
通过RemoteInvocationBasedExporter.invoke处理请求。
Dubbo 源码分析
由于HttpInvokerServiceExporter没有invoke方法,因此会调用父类DefaultRemoteInvocationExecutor的invoke方法,调用RemoteInvocation.invoke
Dubbo 源码分析
最后调用RemoteInvocation#invoke,通过反射执行方法。

总结

第一次对框架的源码进行分析,可能有一些地方没有分析清楚,不过了解到这种程度对于分析漏洞应该够用了。

参考资料

  • https://dubbo.apache.org/zh/docs/

  • https://blog.csdn.net/qq_35190492/article/details/108461885

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月29日03:14:01
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Dubbo 源码分析https://cn-sec.com/archives/814855.html

发表评论

匿名网友 填写信息