最近准备对Dubbo的历史漏洞进行分析,但觉得不懂Dubbo的设计和实现直接去分析漏洞比较困难,所以在分析漏洞前先分析Dubbo的源码及实现,值得一提的是Dubbo的官网也有非常详细的源码分析的过程。
SPI机制及实现
Dubbo的SPI是对JDK自身SPI的扩展实现,增加了IOC和AOP的功能,是Dubbo实现的核心,Dubbo SPI需要的配置文件放在/meta-inf/dubbo
目录下,通过键值对的方式配置,如下所示:
1 | adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory |
Dubbo的SPI和JDK自身的SPI对比如下,这也是Dubbo没有选择使用JDK自带SPI的原因。
可以通过@SPI
注解将接口声明由Dubbo的SPI机制加载实现类。
Dubbo如何实现SPI?
ExtensionLoader
是Dubbo SPI实现的核心类,每个定义的spi的接口都会构建一个ExtensionLoader实例。一般通过ExtensionLoader.getExtensionLoader
获取ExtensionLoader实例。
getExtensionLoader
首先判断是否为接口类型并且由@SPI
注解修饰,也就是说只有@SPI
修饰的接口才会由Dubbo的SPI机制去寻找实现类。下面会通过EXTENSION_LOADERS
寻找是否已经有loader的实例,没有的话会创建一个并添加到EXTENSION_LOADERS
中。
下面分析ExtensionLoader
构造方法,如果type类型不为ExtensionFactory
则先执行ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()
getAdaptiveExtension
首先从缓存中获取实例,没有则通过createAdaptiveExtension
创建实例。
createAdaptiveExtension
首先通过 getAdaptiveExtensionClass().newInstance()
创建实例,再通过injectExtension
包装。
getAdaptiveExtensionClass
首先通过getExtensionClasses
获取Class,找不到则通过createAdaptiveExtensionClass
创建。
getExtensionClasses
首先通过缓存获取Class获取不到则通过loadExtensionClasses
方法获取,获取后放到classes
Map中。
loadExtensionClasses
首先获取SPI注解的属性值放到缓存中,下面通过loadDirectory
从配置文件中加载Class,主要从META-INF/dubbo/
、META-INF/services/
、META-INF/dubbo/internal
几个目录下加载文件。
根据dir和type作为文件名加载资源,并通过loadResource
加载类的信息并放到extensionClasses
中。
loadResource
中读取文件并解析文件内容获取name
和接口实现类的名称
,下面通过loadClass
加载。
在loadClass
中首先检查clazz
是否是type的实现类,再去检测clazz的接口是否有Adaptive
注解存在的话放到将类放到cachedAdaptiveClass
缓存中,下面再通过是否有参数为clazz
的构造方法,有的话将clazz存到cachedWrapperClasses
中,下面查看实现类是否有Extension
注解,有的话取出这个注解的值并赋值给name。下面获取name的值,可以通过xxx,xxx,xx=xxx.com
等形式传入多个name,并通过saveInExtensionClass
将name
和class
的值保存到extensionClasses
中。
下面在回到getAdaptiveExtensionClass
方法中,首先在缓存中查找,找不到则会通过createAdaptiveExtensionClass
创建Class。
createAdaptiveExtensionClass
首先根据type
和SPI配置的value
的值生成Adaptive包装类并编译为Class,也就是说我们获取的类型不是配置的实现类对象,而是Adaptive包装类对象。
生成代码的逻辑比较复杂,我们就不深入分析了,不过我们可以拿到生成的代码,可以看看生成的代码主要做了什么。首先它是type接口的实现类,如果接口中的方法没有通过Adaptive
修饰,则直接抛出异常。
对于Adaptive
注解修饰的方法则会生成实现,首先检查Invoker的url是否为空,再获取协议信息,如果没有配置协议则默认使用dubbo
协议,下面获取Protocol的实现类并执行实现类的export方法,其实也就是对export进行了一些包装,在执行export前加了一些验证逻辑。
refer
方法逻辑类似,只是最后调用实现类的refer
方法。
下面我们再回到createAdaptiveExtension
方法中,通过getAdaptiveExtensionClass()
已经拿到了动态创建的Adaptive
类并通过newInstance创建对象
,下面通过injectExtension
完成依赖注入。
如何实现IOC?
injectExtension
获取setter方法,并通过objectFactory.getExtension(pt, property);
获取需要注入的对象,通过反射调用setter方法完成依赖注入。
objectFactory
可能是下面三种实现类,也就是说除了可以通过Spi
获取注入的对象也可以从spring中获取注入对象。而AdaptiveExtensionFactory
则会循环调用多个factory获取对象。
一般objectFactory经过初始化后会封装为AdaptiveExtensionFactory
并且包含了spi
和spring
两个工厂,也就是说默认会通过spi
和spring
两种方式加载需要注入的对象。
为什么可以得到AdaptiveExtentionFactory?
在容器启动时,会解析<dubbo:service
对象,并创建一个serviceBean实例,这个实例是serviceConfig
的子类,创建serviceBean
实例的过程中也会执行父类的static属性,会执行如下操作。
跳过一些已经分析过的步骤,在执行ExtensionLoader
构造方法时,会判断类型是否为ExtensionFactory
类型,如果不是则会执行后面的代码。
进入getExtensionLoader
方法,如果缓存中没有extensionLoader则重新创建一个,也就是说这里还会再调用一次ExtensionLoader
的构造方法。
由于这次type
的类型为ExtensionFactory
,所以会返回一个ExtensionLoader
对象但是此时objectFactory
属性为空。
下面通过getAdaptiveExtesion
获取ExtensionFactory
的实现类,同样中间的过程不分析了,主要关注在loadExtensionClasses
中获取了ExtensionFactory
的实现类。
但是ExtensionFactory
中明明配置了三个实现类,为什么加载后变成了两个而没有AdaptiveExtensionFactory
。
我们跟进资源加载部分的代码,可以看到确实读取到了AdaptiveExtensionFactory
。
在loadClass
中,由于AdaptiveExtensionFactory
实现类由Adaptive
注解修饰,因此会该类到缓存cachedAdaptiveClass
中并返回,并不会执行后面的saveInExtensionClass
方法。
在执行完getExtensionClasses
后,会判断cachedAdaptiveClass
中是否有值,有的话则直接返回,所以这里其实通过getAdaptiveExtensionClass
返回了AdaptiveExtensionFactory
类。
所以下面是创建了AdaptiveExtensionFactory
的实例。
而在AdaptiveExtensionFactory
的构造方法中,通过 loader.getSupportedExtensions()
获取扩展名,并通过loader.getExtension("spring")
获取对应的工厂封装到factorties中。
除了动态生成ProtocolAdaptive包装类外,还生成了proxyFactoryAdaptive
包装类。
在Dubbo中主要使用了两种代理方式,即JDK和javassist。
proxyFactoryAdaptive
中主要实现了getProxy
和getInvoker
方法,如果没有配置代理则默认使用javaassist动态代理。
getInvoker
中则根据传入的参数完成方法的调用。
标签解析过程
上面分析了Dubbo SPI机制的实现过程,下面分析下Dubbo 中配置的标签是如何解析的?
NamespaceHandler
用来解析Spring遇到的所有这个特定的namespace配置文件中的所有elements,Dubbo 实现了DubboNamespaceHandler
作为Dubbo标签中属性的处理器,在它的init方法中,配置了不同element的标签解析器。
并且Dubbo扩展了BeanDefinitionParser
,自定义了DubboBeanDefinitionParser
将标签中配置的属性值设置到Bean中,会对bean的属性赋值。
服务导出过程
通过DubboNamespaceHandler
中的配置,可以知道service元素的配置信息会被方法ServiceBean
中。
而ServiceBean
中实现了ApplicationListener
监听器接口,每当ApplicationContext发布ApplicationEvent时,实现ApplicationListener的Bean将自动被触发。
所以会触发ServiceBean.onApplicationEvent
方法。
在ServiceBean.onApplicationEvent
中通过export方法导出服务。
在ServiceBean#export
中,调用父类ServiceConfig.export
,首先判断做了一些检查,检测是否导出,和延时导出后通过doExport
完成导出。
doExport
中首先判断是否已经导出过了,再判断是否设置path如果没有则将interfaceName作为path并调用doExportUrls方法。
doExportUrls
首先通过loadRegistries
加载注册中心的地址,其次通过buildKey
获取接口名,将接口名、实现类实例、接口Class封装到ProviderModel中。再通过initProviderModel
将serviceName和providerModel 放到providedServices
中。最后通过doExportUrlsFor1Protocol
导出服务。
doExportUrlsFor1Protocol
首先获取name属性值,为空则默认name为dubbo。创建一个存放参数的map,将一些配置的参数放到map中。
下面通过接口Class构造了接口的包装类,通过包装类获取所有的method,并将methed添加到map中。
下面获取host和port,并通过这些信息和map中的信息构造一个org.apache.dubbo.common.URL
对象,其中path为interfaceName,map中保存的信息被当作参数。
当scope属性没有配置时,则默认先通过exportLocal
先发布到本地,再发布到远程。
本地导出
本地导出主要在exportLocal
中实现,首先判断协议名是否为injvm
,如果是则表示已经导出过了,不再进行导出。下面构建本地导出的url,获取Invoker并导出。
这里的proxyFacory
为之前分析SPI机制时动态生成的ProxyFactoryAdaptive类,它的getInvoker
方法如下,默认情况下会使用javasist代理。
JavassistProxyFactory#getInvoker
首先创建了实现类的包装类,再创建了AbstractProxyInvoker
对象并重写了doInvoke方法
而protocol
也是在SPI机制动态生成的Adaptor,其export方法如下,
而具体调用哪个Protocol
的export方法是由(org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName)
的返回结果决定的,如果我们配置的是是injvm
协议,则返回injvmProtocol
的包装对象。
在ProtocolFilterWrapper#export
中,首先判断是否是registry
协议,如果是则直接导出否则通过buildInvokerChain
构建过滤器链后再导出。
buildInvokerChain
构造过滤器链,只有当左右的Filter执行完后才会执行invoker的invoke方法。
InjvmProtocol#export
创建InjvmExporter
对象完成本地导出。
远程导出
远程导出首先还是获取Invoker,再将Invoker和serviceBean封装到DelegateProviderMetaDataInvoker
中,最后调用export方法导出。
服务导出
下面分析RegistryProtocol#export
,在export方法中,主要通过RegistryProtocol#doLocalExport
完成服务导出。首先从Invoker中获取key,其次创建了InvokerDelegate
作为Invoker的委托类,最终通过protocol.export完成导出
。
由于我这里使用的是http协议,但HttpProtocol没有export方法,因此会调用父类AbstractProxyProtocol
的export方法。在AbstractProxyProtocol#export
中,首先判断是否已经导出过,如果没有则通过doExport
完成导出。
在HttpProtocol#doExport
中,首先互获取绑定地址,从serverMap
缓存中获取server,没有的话通过bind创建server。
bind的同时创建了InternalHandler
,其中handle方法内容为当通过post请求时,会通过HttpInvokerServiceExporter.handleRequest
处理请求。
httpBinder也是Adaptive类,其内容如下,在export方法中,从url中获取server属性并根据server属性得到HttpBinder的实现类,并调用实现类的bind方法。如果没有配置server属性则默认为jetty。
1 | public class HttpBinder$Adaptive implements org.apache.dubbo.remoting.http.HttpBinder { |
由于我们配置的server为tomcat因此会调用TomcatHttpBinder#bind
方法,创建一个TomcatHttpServer
.
下面构造Tomcat服务需要的一些参数,并且动态创建一个servlet,启动tomcat服务器。
启动server后,获取path,并将path作为key,通过createExporter
创建的HttpInvokerServiceExporter
作为值put到skeletonMap中。
给HttpInvokerServiceExporter
添加接口信息和实现类.
最后创建一个Runnable对象并返回。
回到AbstractProxyProtocol#export
中,得到runnable对象后,创建AbstractExporter
对象并返回,将exporter放到缓存中后返回。
服务注册
首先获取registry实例,并且获取providerUrl,通过registerProvider
将provider注册到providerInvokers
中。
下面调用register方法进行服务注册。
register方法中主要通过doRegister完成注册。
由于我们使用的是zookeeper作为注册中心,所以会通过ZookeeperRegistry#doRegister
完成服务注册。
服务引用过程
服务引用相当于生成了一个代理类,这个代理类可以给我们屏蔽远程调用的细节。
服务引用分为三种,即本地引用,远程引用和注册中心引用。
下面介绍来自字节面试:dubbo的服务引用过程
1 | 本地引入不知道大家是否还有印象,之前服务暴露的流程每个服务都会通过搞一个本地暴露,走 injvm 协议(当然你要是 scope = remote 就没本地引用了),因为存在一个服务端既是 Provider 又是 Consumer 的情况,然后有可能自己会调用自己的服务,因此就弄了一个本地引入,这样就避免了远程网络调用的开销。 |
服务引用主要通过ReferenceBean
来实现,这个类实现了FactoryBean接口,在spring容器初始化时会调用ReferenceBean#getObject
方法。
get
先调用checkAndUpdateSubConfigs
检查属性值是否正确,再调用init
完成服务引用。
init
方法首先将很多信息封装到map中,再调用createProxy
创建代理。
判断是否为本地调用,如果为本地调用,则调用refprotocol.refer
创建一个InjvmInvoker对象返回。
判断url是否为空,不为空则判断是远程引用还是注册中心引用。
获取注册中心的地址,判断是否配置监控中心,如果没有则跳过,最后向url中加入refer参数。
下面当url只有一个时则直接调用prtocol.refer
生成invoker。如果有多个url则取最后一个registry的url作为registryURL,获取多个invoker添加到invokers中,并将invokers封装到StaticDirectory中,通过cluster封装为一个invoker,而这个invoker的地址为registryURL。
再看下refprotocol.refer
是如何做的,由于我们使用的是registry协议,所以会调用RegistryProtocol#refer
,首先获取url中的registry参数,并将参数的内容设置为url的协议名重新构造url,其次获取registry实例,如果要调用的接口名是RegistryService的实例,则直接构造Invoker返回,最后对group参数做处理,如果没有则直接调用doRefer
完成核心的服务引用的逻辑。
doRefer
首先创建了RegistryDirectory
对象,RegistryDirectory 是一种动态服务目录,实现了 NotifyListener 接口。当注册中心服务配置发生变化后,RegistryDirectory 可收到与当前服务相关的变化。接下来构造consumer的url并注册到注册中心,通过subscribe
订阅provider和router等服务,订阅了之后 RegistryDirectory 会收到这几个节点下的信息,触发Invoker的生成。最后通过cluster封装directory得到Invoker,将Consumer的信息添加到ProviderConsumerRegTable
后返回Invoker。
现在主要关注subscribe
订阅方法,订阅过程中会调用对应协议的refer方法,由于我们配置的是http协议,但HttpProtocol
没有实现refer方法,因此会调用父类AbstractProxyProtocol.refer
。
AbstractProxyProtocol#refer
首先调用HttpInvoker.doRefer
获取http调用客户端代理类对象,并通过getInvoker
将代理类转换为Invoker,创建AbstractInvoker
对象并实现doInvoke方法,在doInvoke中调用invoker.invoke方法,完成服务调用并获取返回结果。
HttpInvoker.doRefer
创建了HttpInvokerProxyFactoryBean
。
在spring中提供了HttpInvoker 通过HTTP协议实现RPC调用,Spring HttpInvoker使用Java序列化来序列化参数和返回值,然后基于HTTP协议传输经序列化后的对象。Spring HttpInvoker
的服务端和客户端分别由HttpInvokerServiceExporter
和HttpInvokerProxyFactoryBean
实现。
服务端处理如下:
客户端处理如下:
创建HttpInvokerProxyFactoryBean
对象后重写了createRemoteInvocation
方法,根据不同的dubbo版本创建的不同的RemoteInvocation
对象。
下面设置url和intercepte属性,并且创建了发送请求的客户端并封装到httpProxyFactoryBean中。创建SimpleHttpInvokerRequestExcutor
对象并设置到httpProxyFactoryBean
中。
下面调用afterPropertiesSet
方法,创建ProxyFactory
对象,通过getProxy
获取AOP代理对象。
这里传入的interceptor是HttpInvokerProxyFactoryBean
,这个Bean的父类HttpInvokerClientInterceptor
继承了MethodInterceptor
,因此这里相当于添加了一个环绕通知。
最后调用getObject实际上是返回HttpProxyFactoryBean
的代理对象。
服务调用过程
服务调用是通过消费者的代理对象发起的,这个代理对象中包含了之前创建的服务引用。
查看org.apache.dubbo.rpc.proxy.InvokerInvocationHandler#invoke
,封装调用的方法名和参数到RpcInvocation
中,调用MockClusterInvoker.invoke
org.apache.dubbo.rpc.cluster.support.wrapper.MockClusterInvoker#invoke
首先判断是否使用Mock机制,如果没有则调用AbstractClusterInvoker.invoke
AbstractClusterInvoker.invoke
得到Invoker,初始化负载均衡调用FailoverClusterInvoker.doInvoke
FailoverClusterInvoker.doInvoke
利用负载均衡策略选择一个invoker,并通过RpcContext
记录调用过的Invoker,最后执行invoker的invoke方法。
回想一下服务引用的过程,真正执行请求的Invoker被封装为AbstractInvoker
,所以会调用AbstractInvoker.invoke
方法,设置invocation的invoker,并调用doInvoke方法。
我们实现的AbstractProxyProtocol
重写了doInvoke方法,执行代理类的invoke方法。
这个代理类是AbstractProxyInvoker
的实例,因此会调用AbstractProxyInvoker.invoke
在AbstractProxyInvoker.invoke
调用了重写的doInvoke
方法,也就是通过wapper.invokeMethod
调用。也就是调用proxy的methodname方法。
由于我们添加了环绕通知,因此会调用HttpInvokerClientInterceptor.invoke
执行调用请求。
在HttpInvokerClientInterceptor#executeRequest
中获取executer并执行executeRequest
方法。
由于我们之前服务引用在HttpInvokerProxyFactoryBean
中设置的是SimpleHttpInvokerRequestExecutor
,但SimpleHttpInvokerRequestExecutor
中没有executeRequest
,因此调用父类AbstractHttpInvokerRequestExecutor.executeRequest
,在这个类中,将RemoteInvocation
进行序列化后调用SimpleHttpInvokerRequestExecutor#doExecuteRequest
完成请求发送。
之前在服务导出时我们已经开启了tomcat服务并且创建了internalHandler
设置到DispatcherServlet中,当接收到请求时,将通过internalHandler.handle
处理请求。
获取HttpInvokerServiceExporter
处理request请求。
通过readRemoteInvocation
将传入的数据反序列化为RemoteInvocation
对象,调用invokeAndCreateResult
完成请求处理。
通过RemoteInvocationBasedExporter.invoke
处理请求。
由于HttpInvokerServiceExporter
没有invoke方法,因此会调用父类DefaultRemoteInvocationExecutor
的invoke方法,调用RemoteInvocation.invoke
最后调用RemoteInvocation#invoke
,通过反射执行方法。
总结
第一次对框架的源码进行分析,可能有一些地方没有分析清楚,不过了解到这种程度对于分析漏洞应该够用了。