1.简介
得益与Spring的“微内核与扩展机制“,第三方框架能够很方便的和Spring进行集成。
这篇笔记,我们就通过分析"Dubbo通过Spring导出服务的过程“,来见微知著,体会下Spring是如何和第三方框架进行集成的,关注Spring提供了哪些扩展点。
相关组件版本:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<!-- 使用0.11版本会有兼容问题 -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
<exclusions>
<exclusion>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</exclusion>
</exclusions>
</dependency>
2.ServiceBean.class
通过查阅Dubbo官方文档,知道了Dubbo导出服务的入口方式是ServiceBean的onApplicationEvent方法。
先说明下ServiceBean其实就是每个具体的dubbo服务的封装,比如下面这个dubbo provider的配置文件:
<?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="hello-world-app" />
<!-- 使用zk广播注册中心暴露服务地址 -->
<dubbo:registry address="zookeeper://127.0.0.1:2181" />
<!-- 用dubbo协议在20880端口暴露服务 -->
<dubbo:protocol name="dubbo" port="20880" />
<!-- 声明需要暴露的服务接口 -->
<dubbo:service interface="com.huang.dubbo.api.DemoService" ref="demoServiceImpl" />
<!-- <!– 和本地bean一样实现服务 –>-->
<!-- <bean id="demoService" class="com.huang.dubboprovider.service.DemoServiceImpl" />-->
</beans>
” <dubbo:service interface="com.huang.dubbo.api.DemoService" ref="demoServiceImpl" />“最终就会被封装成一个ServiceBean。
onApplicationEvent方法是ServiceBean实现于ApplicationListener接口,ApplicationListener就是是Spring提供的一个扩展点,实现了这个接口的spring bean,能够订阅Spring容器的指定事件,当Spring广播指定的事件之后,对应的spring bean就可以调用onApplicationEvent方法来执行自己事先计划好的逻辑。
我们在ServiceBean的onApplicationEvent方法上打上断点,启动Spring项目。通过调用栈可以发现,Spring容器在启动的过程中会调用refreshContext方法,这个方法内会执行很多关键的逻辑,本篇笔记就先不赘述了,我们只要知道Spring执行refreshContext完毕(finishRefresh)之后,Spring容器会广播一个ContextRefreshedEvent事件,ServiceBean订阅了这个事件,就会执行相应的逻辑,这其实是一个观察者模式的实现。Spring通过观察者模式,让第三方框架通过指定的事件和Spring容器进行交互。最后ServiceBean调用export()方法,来将对应的dubbo服务注册到注册中心上去。
Spring应用的事件都继承自ApplicationEvent,除了ContextRefreshedEvent之外,还有其他很多可以被监听的事件,开发者可以根据需要,通过订阅这些在Spring应用生命周期中的事件,来与Spring进行交互且无需修改Spring的代码。这就是Spring扩展机制的美妙之处XD。
3.Spring容器创建ServiceBean过程
从上文第二章我们已经知道,dubbo服务是通过ServiceBean导出并注册到指定的注册中心上去的,这个算是”果“,还要搞清楚”因“才行。什么是”因“呢?Spring容器如何从dubbo的provider.xml文件中读取配置,并创建对应的ServiceBean实例,这个就是”因“,明白了这个,我们才能够拍着胸脯说:”我知道dubbo是如何和Spring进行集成的!“。
dubbo要暴露的服务配置在provider.xml文件中(当然dubbo也有提供java代码配置服务的方式,本篇笔记就不多赘述。)。我们都知道,Spring除了能够通过代码注解来配置创建bean,也能够通过xml文件来配置bean,然后让Spring容器读取xml文件来创建bean。这个时候你要说了,Spring bean的xml文件中,配置一个bean的节点不应该用”“标签吗,怎么dubbo的配置文件中,用的标签是”dubbo:service/“?这个其实是Spring的XML Schema-based configuration机制,想详细了解的可以看下官方文档,简单点说,这个机制的作用就是让bean配置文件更加的通用且方便。用户可以定义自己的xml标签,然后通过自定义的XSD文件来校验。Spring容器在读取用户自定义的标签时肯定是不认识的,所以用户还需要为自定义的xml文件实现NamespaceHandler和BeanDefinitionParser,来解析xml文件中的标签。
有了上述的知识铺垫之后,我们来,看看ServiceBean是如何被Spring读取并注入到Spring容器中的。这里就不完全把debug时的各种尝试给描述出来了。因为一开始debug也没思路,只能抓着已知的ServiceBean来下手,看看dubbo代码中有哪些地方引用了ServiceBean,再去相关(看包名、类名、实现了哪些接口、父类、方法名)的代码上打上断点来调试,把这个调试的过程全部说出来可太费篇幅了。
我是使用@ImportResource来让Spring容器加载dubbo的配置文件到容器中。
package com.huang.dubboprovider.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
@Configuration
@ImportResource({"classpath:dubbo-provider.xml"})
public class ImportDubboProviderConfig {
}
dubbo-provider.xml中各个ServiceBean实例被创建被注入Spring容器的过程如下图所示(源码就不贴了,贴上去的话啰啰嗦嗦一大段,而且看起来也很费劲。只给个大概的流程就好,说清楚关键类名,以后再次debug也很方便,不会晕头转向):
这个过程中,dubbo通过Spring的SPI,将dubbo xml文件的NamespaceHandler指定成了DubboNamespaceHandler,具体过程如下:
DefaultNamespaceHandlerResolver,读取当前classloader中所有的META-INF/spring.handlers文件,spring.handlers中记录了namespaceUri和具体NamespaceHandler之间的映射关系。
所以解析dubbo-provider.xml的时候会自动使用DubboNamespaceHandler。
DubboNamespaceHandler继承自NamespaceHandlerSupport,NamespaceHandlerSupport实现了NamespaceHandler接口。NamespaceHandler中只有一个init方法,DubboNamespaceHandler在init方法中,将xml文件中各种类型的标签标签和他们对应的BeanDefinitionParser建立起映射关系,保存到NamespaceHandlerSupport.parsers(一个HashMap)中。
然后通过NamespaceHandlerSupport.parsers给dubbo xml文件总不同类型的标签选择不同类型的DubboBeanDefinitionParser来解析。Spring在这个过程中,给用户留下了两个”钩子“,一个是NamespaceHandler的inti方法,另一个是NamespaceHandlerSupport的parsers字段,这种留”钩子“的方式,本质上来说应该是”模板方法模式“,提供方(父类)创建好模板,留下钩子共使用方(子类)实现。
4.总结
通过分析dubbo集成Spring,将服务导出并注册到注册中心的过程。我们知道了,在这个过程中Spring至少提供了三种方式,方便用户扩展或者自定义Spring的行为:事件机制(观察者模式、发布订阅模式)、SPI机制、模板方法模式(钩子),当然Spring的扩展点可远不止上述三种,不过都万变不离其宗。核心的方式还是”面向接口编程“,将功能细分抽象成接口,用户通过接口扩展或者改变Spring的行为。好好体会”面向接口编程“这种方式,对于我们构建易于扩展、维护的程序会很有帮助。
5.源码地址
dubbo与Springboot集成项目的源码地址(拿来学习dubbo,debug用的):github.com/ambition080…
6.参考
扩展Spring系列(1)--Spring 的微内核与FactoryBean扩展机制