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

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 Dubbodubbo-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"
       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">

	<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 插件机制,需要做这么几个事情:

  1. 定义自己业务的类。

  2. 编写 XSD 文件,定义自己的 XML 格式,将文件放在 src/main/resources/META-INF 目录下。

  3. 针对每一个标签,定义一个实现 BeanDefinitionParser 接口的类,在 parse 方法中完成对这个标签的解析工作,将其转化成一个 BeanDefinition 对象。

  4. 继承 NamespaceHandlerSupport 类,在 init() 方法中,使用 registerBeanDefinitionParser() 将标签名称和上面写的 BeanDefinitionParser 实现类之间建起起对应关系。

  5. 创建 src/main/resources/META-INF/spring.schemas 文件,在其中写上: http\://www.diguage.com/schema/diguage/diguage.xsd=META-INF/diguage.xsd,为该 XSD 文件定义唯一的命名空间。

  6. 创建 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 的整合过程。

  1. 相关业务类有 ApplicationConfigModuleConfigRegistryConfigConfigCenterBeanMetadataReportConfigMonitorConfigMetricsConfigSslConfigProviderConfigConsumerConfigProtocolConfigServiceBeanReferenceBean。这些类的命名也都非常讲究,见文知意,与 Dubbo 常见配置可以说是一一对应。

  2. Dubbo 的 XSD 定义在 dubbo.xsd,懂 XSD 的朋友应该都能看出来,这个文件就是规范上一步提到的类的属性的。

  3. DubboBeanDefinitionParser 实现了 BeanDefinitionParser 接口,用于解析 XML 配置,并将其“翻译”为第一步中那些类的对象。另外,还注册了一个 AnnotationBeanDefinitionParser,用来处理 annotation 标签,进而用来处理注解。

  4. DubboNamespaceHandler 继承了 NamespaceHandlerSupport,并且在 init() 方法中完成了对上述类的 DubboBeanDefinitionParser 注册。

  5. 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);
}

这里需要重点关注的是 ReferenceAnnotationBeanPostProcessorDubboBootstrapApplicationListener,前者设计到 Dubbo 注解的处理,后者着牵涉整个 Dubbo 的启动。先在 DubboBootstrapApplicationListeneronApplicationContextEvent 方法上打上断点。后续涉及到时,再具体分析。

然后,我们单步调试,跟进 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 完成了以下功能:

  1. initFrameworkExts() — 初始化框架

  2. startConfigCenter() — 启动配置中心

  3. loadRemoteConfigs() — 加载远程配置

  4. checkGlobalConfigs() — 检查全局配置

  5. startMetadataCenter() — 开始元数据中心,这里特别标明是从 2.7.8 开始的。

  6. initMetadataService() — 初始化元数据服务

  7. initMetadataServiceExports() — 初始化元数据服务导出

  8. initEventListener() — 初始化时间监听。

暂时没有深入研究这些方法的实现。说明也都是直译的方法名。

继续向下执行,进入 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 上找原因。经过研究发现, ServiceConfigBaseorg.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 变量添加元素的。在这个类打断点,你就看到所有添加的变量信息。再次启动服务提供者程序,你会发现上面提到的相关业务类 ApplicationConfigModuleConfigRegistryConfigConfigCenterBeanMetadataReportConfigMonitorConfigMetricsConfigSslConfigProviderConfigConsumerConfigProtocolConfigServiceBeanReferenceBean 都是 AbstractConfig 的子类。换句话说,这些类的实例都会注册到 ConfigManager 中。

洋洋洒洒又写了好长好长。还有很多东西没写呢,比如 Dubbo 注解的集成实现,Dubbo 服务消费者的创建过程。限于篇幅原因,这些内容就放在下一篇文章介绍。