Spring 扩展点实践:整合 Apache Dubbo(一)

在上一篇文章 Spring 扩展点概览及实践 中介绍了 Spring 内部存在的扩展点。 Spring 扩展点实践:整合 MyBATIS 中,D瓜哥带大家了解了一下 MyBATIS 如何利用 Spring 的扩展点实现了与 Spring 的完美整合。现在,学以致用,我们继续来分析一下 Spring 与 Apache Dubbo 的整合流程。
示例程序
Apache Dubbo 仓库中就有很完整的示例。D瓜哥直接拿来使用就不再搭建示例程序了。
首先,需要启动一个 ZooKeeper 实例。查看 Dubbo 的依赖可以看出,最新版代码依赖的 ZooKeeper 是 3.4.13 版。所以,为了最好的兼容性,就要选用 3.4.X 版的 ZooKeeper 服务器。D瓜哥直接使用 Docker 启动 ZooKeeper 了。命令如下:
docker run --rm --name zookeeper -d -p 2181:2181 zookeeper:3.4.14
这次我们使用 Apache Dubbo 的 dubbo-demo/dubbo-demo-xml
示例。
第二步,启动服务提供者程序,找到 DUBBO/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/java/org/apache/dubbo/demo/provider/Application.java
,运行该类。
第三步,运行服务消费者程序,找到 DUBBO/dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-consumer/src/main/java/org/apache/dubbo/demo/consumer/Application.java
,运行该类。
如果没有任何错误,则在终端可以看到 result: async result
输出。
在开始正餐之前,D瓜哥先给大家来个开胃菜。
Spring 插件机制简介
不知道大家有没有想过一个问题:Spring 框架是如何支持越来越多的功能的?
在D瓜哥了解到 Spring 的插件机制后,非常叹服 Spring 精巧的设计和灵活的扩展性。闲言少叙,好戏上演。
这里再问大家一个问题:
<?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:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<context:annotation-config/>
<tx:annotation-driven proxy-target-class="true" order="0"/>
<aop:config>
<aop:advisor pointcut="execution(* ..ITestBean.(..))" advice-ref="txAdvice"/>
</aop:config>
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" timeout="5" read-only="true"/>
<tx:method name="set*"/>
<tx:method name="exceptional"/>
</tx:attributes>
</tx:advice>
<bean id="transactionManager" class="org.springframework.transaction.testfixture.CallCountingTransactionManager"/>
<bean id="testBean" class="org.springframework.beans.testfixture.beans.TestBean"/>
</beans>
这是非常典型的 Spring XML 配置。相信大家都见过。大家有没有想过,Spring 是怎么处理这些不同的命名空间的?如果说 AOP、事务这些是 Spring 内置支持的功能,这样配置,Spring 可以正确解析。但是,Dubbo 的配置又是怎么回事?
要回答这个问题,就要说起 Spring 的插件机制。在 Spring 的插件机制面前,无论是 Dubbo,还是 Spring 的 AOP、事务管理都是人人平等的。它们都是依靠 Spring 的插件机制插拔在 Spring 核心模块之上的。
这篇文章不是专门介绍 Spring 插件机制的。这里抛砖引玉,对 Spring 插件机制做个简介。后续有机会再做更详细的介绍和说明。
要利用 Spring 插件机制,需要做这么几个事情:
-
定义自己业务的类。
-
编写 XSD 文件,定义自己的 XML 格式,将文件放在
src/main/resources/META-INF
目录下。 -
针对每一个标签,定义一个实现
BeanDefinitionParser
接口的类,在parse
方法中完成对这个标签的解析工作,将其转化成一个BeanDefinition
对象。 -
继承
NamespaceHandlerSupport
类,在init()
方法中,使用registerBeanDefinitionParser()
将标签名称和上面写的BeanDefinitionParser
实现类之间建起起对应关系。 -
创建
src/main/resources/META-INF/spring.schemas
文件,在其中写上:http\://www.diguage.com/schema/diguage/diguage.xsd=META-INF/diguage.xsd
,为该 XSD 文件定义唯一的命名空间。 -
创建
src/main/resources/META-INF/spring.handlers
文件,在其中写上:http\://www.diguage.com/schema/diguage=com.diguage.schema.DiguageNamespaceHandler
。
完成上面这些步骤就相当于制作了一个 Spring 插件。这样就可以在 Spring XML 配置文件中,像使用 AOP、事务管理那样来使用这个新插件了。
仔细想想,Spring 的插件机制还是挺简单的:首先,定义一个 Bean 类,然后设计 XSD 文件来对 Bean 的属性进行定义。用户在使用插件时,使用 XML 来定义 Bean 类的属性值,再自定义的 BeanDefinitionParser
实现类将 XML 中的配置信息解析出来,封装在 BeanDefinition
(关于 BeanDefinition
的更多信息,请移步 深入剖析 Spring 核心数据结构:BeanDefinition)。到了 BeanDefinition
之后,Spring 在内部就可以统一处理了。
下面,结合代理来具体说明一下 Apache Dubbo 的实现过程。
Apache Dubbo 插件机制解析
Apache Dubbo 最初就说通过 Spring 插件机制实现了它与 Spring 的整合过程。
-
相关业务类有
ApplicationConfig
、ModuleConfig
、RegistryConfig
、ConfigCenterBean
、MetadataReportConfig
、MonitorConfig
、MetricsConfig
、SslConfig
、ProviderConfig
、ConsumerConfig
、ProtocolConfig
、ServiceBean
和ReferenceBean
。这些类的命名也都非常讲究,见文知意,与 Dubbo 常见配置可以说是一一对应。 -
Dubbo 的 XSD 定义在 dubbo.xsd,懂 XSD 的朋友应该都能看出来,这个文件就是规范上一步提到的类的属性的。
-
DubboBeanDefinitionParser
实现了BeanDefinitionParser
接口,用于解析 XML 配置,并将其“翻译”为第一步中那些类的对象。另外,还注册了一个AnnotationBeanDefinitionParser
,用来处理annotation
标签,进而用来处理注解。 -
DubboNamespaceHandler
继承了NamespaceHandlerSupport
,并且在init()
方法中完成了对上述类的DubboBeanDefinitionParser
注册。 -
在
dubbo-config/dubbo-config-spring/src/main/resources/META-INF
目录下,有spring.schemas
文件和spring.handlers
文件。
下面以调试跟进的方式来分析整个处理过程。
Apache Dubbo 配置解析
这里使用示例程序中的配置文件:
dubbo-demo/dubbo-demo-xml/dubbo-demo-xml-provider/src/main/resources/spring/dubbo-provider.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://dubbo.apache.org/schema/dubbo
http://dubbo.apache.org/schema/dubbo/dubbo.xsd">
<dubbo:application metadata-type="remote" name="demo-provider"/>
<dubbo:metadata-report address="zookeeper://127.0.0.1:2181"/>
<dubbo:registry address="zookeeper://127.0.0.1:2181"/>
<dubbo:protocol name="dubbo"/>
<bean id="demoService" class="org.apache.dubbo.demo.provider.DemoServiceImpl"/>
<dubbo:service interface="org.apache.dubbo.demo.DemoService" ref="demoService"/>
</beans>
在 org.apache.dubbo.config.spring.schema.DubboNamespaceHandler#init
方法、 org.apache.dubbo.config.spring.schema.DubboNamespaceHandler#parse
方法 和 org.apache.dubbo.config.spring.schema.DubboBeanDefinitionParser#parse(Element, ParserContext)
方法打断点开始调试。注意:这三个方法都是重载方法,很容易识别。
打好断点后重启服务提供者程序,程序会在 init()
方法处暂停:
org.apache.dubbo.config.spring.schema.DubboNamespaceHandler#init
@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("ssl", new DubboBeanDefinitionParser(SslConfig.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());
}
从这里可以明显看到,都注册哪些 BeanDefinitionParser
,都需要处理哪些标签。点击 registerBeanDefinitionParser
方法就可以看出,所谓的“注册”其实就是将它们放在了 org.springframework.beans.factory.xml.NamespaceHandlerSupport#Map<String, BeanDefinitionParser> parsers
变量中。
这里不要深究,继续向下执行,就会到了 DubboNamespaceHandler#parse
方法:
org.apache.dubbo.config.spring.schema.DubboNamespaceHandler#parse
@Override
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionRegistry registry = parserContext.getRegistry();
registerAnnotationConfigProcessors(registry);
/**
* @since 2.7.8
* issue : https://github.com/apache/dubbo/issues/6275
*/
registerCommonBeans(registry);
BeanDefinition beanDefinition = super.parse(element, parserContext);
setSource(beanDefinition);
return beanDefinition;
}
这里,我们需要注意的是 registerCommonBeans(registry)
方法:
org.apache.dubbo.config.spring.util.DubboBeanUtils#registerCommonBeans
/**
* Register the common beans
*
* @param registry {@link BeanDefinitionRegistry}
* @see ReferenceAnnotationBeanPostProcessor
* @see DubboConfigDefaultPropertyValueBeanPostProcessor
* @see DubboConfigAliasPostProcessor
* @see DubboLifecycleComponentApplicationListener
* @see DubboBootstrapApplicationListener
*/
static void registerCommonBeans(BeanDefinitionRegistry registry) {
// Since 2.5.7 Register @Reference Annotation Bean Processor as an infrastructure Bean
registerInfrastructureBean(registry, ReferenceAnnotationBeanPostProcessor.BEAN_NAME,
ReferenceAnnotationBeanPostProcessor.class);
// Since 2.7.4 [Feature] https://github.com/apache/dubbo/issues/5093
registerInfrastructureBean(registry, DubboConfigAliasPostProcessor.BEAN_NAME,
DubboConfigAliasPostProcessor.class);
// Since 2.7.5 Register DubboLifecycleComponentApplicationListener as an infrastructure Bean
registerInfrastructureBean(registry, DubboLifecycleComponentApplicationListener.BEAN_NAME,
DubboLifecycleComponentApplicationListener.class);
// Since 2.7.4 Register DubboBootstrapApplicationListener as an infrastructure Bean
registerInfrastructureBean(registry, DubboBootstrapApplicationListener.BEAN_NAME,
DubboBootstrapApplicationListener.class);
// Since 2.7.6 Register DubboConfigDefaultPropertyValueBeanPostProcessor as an infrastructure Bean
registerInfrastructureBean(registry, DubboConfigDefaultPropertyValueBeanPostProcessor.BEAN_NAME,
DubboConfigDefaultPropertyValueBeanPostProcessor.class);
}
这里需要重点关注的是 ReferenceAnnotationBeanPostProcessor
和 DubboBootstrapApplicationListener
,前者设计到 Dubbo 注解的处理,后者着牵涉整个 Dubbo 的启动。先在 DubboBootstrapApplicationListener
的 onApplicationContextEvent
方法上打上断点。后续涉及到时,再具体分析。
然后,我们单步调试,跟进 BeanDefinition beanDefinition = super.parse(element, parserContext);
这个调用中:
org.springframework.beans.factory.xml.NamespaceHandlerSupport
/
* Parses the supplied {@link Element} by delegating to the {@link BeanDefinitionParser} that is
* registered for that {@link Element}.
*/
@Override
@Nullable
public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanDefinitionParser parser = findParserForElement(element, parserContext);
return (parser != null ? parser.parse(element, parserContext) : null);
}
/
* Locates the {@link BeanDefinitionParser} from the register implementations using
* the local name of the supplied {@link Element}.
*/
@Nullable
private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) {
String localName = parserContext.getDelegate().getLocalName(element);
BeanDefinitionParser parser = this.parsers.get(localName);
if (parser == null) {
parserContext.getReaderContext().fatal(
"Cannot locate BeanDefinitionParser for element [" + localName + "]", element);
}
return parser;
}
结合上面的 init()
,上面是“放”,现在是根据标签名称来“拿”。这样就找到每个标签对应的 BeanDefinitionParser
。这些 BeanDefinitionParser
的作用就是处理对应的标签并将其转化为 BeanDefinition
。
Dubbo XML 配置的解析就这么些,后续的过程要依赖 Spring 的流程了。
Dubbo 暴露服务提供者的过程
让程序继续执行,就到了我们上面打断点的地方: DubboBootstrapApplicationListener#onApplicationContextEvent
。一路单步调试跟下去,就到了 DubboBootstrap#start
方法。到这一步,Dubbo 就开始启动了。
start()
方法中,调用了 DubboBootstrap#initialize
方法,这个方法就有点像 Spring 的 AbstractApplicationContext#refresh
方法。如果分析 Dubbo 的源代码,这必定是一个好的入口。在 initialize()
方法中,Dubbo 完成了以下功能:
-
initFrameworkExts()
— 初始化框架 -
startConfigCenter()
— 启动配置中心 -
loadRemoteConfigs()
— 加载远程配置 -
checkGlobalConfigs()
— 检查全局配置 -
startMetadataCenter()
— 开始元数据中心,这里特别标明是从 2.7.8 开始的。 -
initMetadataService()
— 初始化元数据服务 -
initMetadataServiceExports()
— 初始化元数据服务导出 -
initEventListener()
— 初始化时间监听。
Warning
|
暂时没有深入研究这些方法的实现。说明也都是直译的方法名。 |
继续向下执行,进入 DubboBootstrap#exportServices
方法:
org.apache.dubbo.config.bootstrap.DubboBootstrap#exportServices
private void exportServices() {
configManager.getServices().forEach(sc -> {
// TODO, compatible with ServiceConfig.export()
ServiceConfig serviceConfig = (ServiceConfig) sc;
serviceConfig.setBootstrap(this);
if (exportAsync) {
ExecutorService executor = executorRepository.getServiceExporterExecutor();
Future<?> future = executor.submit(() -> {
sc.export();
exportedServices.add(sc);
});
asyncExportingFutures.add(future);
} else {
sc.export();
exportedServices.add(sc);
}
});
}
在这里可以清楚看到,Dubbo 通过 org.apache.dubbo.config.ServiceConfig#export
方法把服务暴露到注册中心的。由于这不是 Dubbo 源码分析,所以,实现细节就不再介绍了。
不知道大家有没有一个疑问:这里的 configManager.getServices()
是如何获取带业务实现类对象呢?
要回答这个问题,需要查看一下 configManager.getServices()
返回的是 Collection<ServiceConfigBase>
对象。我们就从 ServiceConfigBase
上找原因。经过研究发现, ServiceConfigBase
是 org.apache.dubbo.config.AbstractConfig
的子类,而 AbstractConfig
中有一个 addIntoConfigManager
方法如下:
org.apache.dubbo.config.AbstractConfig#addIntoConfigManager
@PostConstruct
public void addIntoConfigManager() {
ApplicationModel.getConfigManager().addConfig(this);
}
阅读过 Spring Bean 生命周期概述 文章的朋友应该都清楚,使用 @PostConstruct
的方法会在 Bean 创建过程中,由 AbstractAutowireCapableBeanFactory#invokeInitMethods
方法来统一调用。所以,如果在上面这个方法中打断点,就可以看到调用过程了。
另外,这里给大家介绍一个小技巧:追本溯源,现在开始。从上面的 configManager.getServices()
开始,一步一步打开源代码就会发现, 这些数据是从 org.apache.dubbo.config.context.ConfigManager#configsCache
变量中获取的,那就在这个类中搜 configsCache
,找到向这个变量添加元素的地方,会找到如下方法:
org.apache.dubbo.config.context.ConfigManager#addConfig(AbstractConfig, boolean)
protected void addConfig(AbstractConfig config, boolean unique) {
if (config == null) {
return;
}
write(() -> {
Map<String, AbstractConfig> configsMap = configsCache.computeIfAbsent(getTagName(config.getClass()), type -> newMap());
addIfAbsent(config, configsMap, unique);
});
}
而且,整个类中,这一个地方是向 configsCache
变量添加元素的。在这个类打断点,你就看到所有添加的变量信息。再次启动服务提供者程序,你会发现上面提到的相关业务类 ApplicationConfig
、 ModuleConfig
、 RegistryConfig
、 ConfigCenterBean
、 MetadataReportConfig
、 MonitorConfig
、 MetricsConfig
、 SslConfig
、 ProviderConfig
、 ConsumerConfig
、 ProtocolConfig
、 ServiceBean
和 ReferenceBean
都是 AbstractConfig
的子类。换句话说,这些类的实例都会注册到 ConfigManager
中。
洋洋洒洒又写了好长好长。还有很多东西没写呢,比如 Dubbo 注解的集成实现,Dubbo 服务消费者的创建过程。限于篇幅原因,这些内容就放在下一篇文章介绍。