Dubbo源碼之Spring整合

  • 2019 年 12 月 13 日
  • 筆記

本文主要介紹了在不同的配置模式下,dubbo與spring整合的原理,即:xml配置、註解配置、自動化配置 三種模式下的配置生效原理。

XML啟動

Schema擴展機制

Spring提供了 Schema 擴展機制,用戶可以自定義 Schema 文件,並自定義 Schema 解析器,然後集成到SpringIOC容器中。 創建自定義擴展,主要有以下步驟:

  1. 創建 Schema 文件,描述自定義的合法構建模組,也就是xsd文件,主要用於定義數據約束;
  2. 自定義個處理器類,並實現NamespaceHandler介面,在裡面註冊各個標籤對應的BeanDefinitionParser;
  3. 自定義一個或多個解析器,實現 BeanDefinitionParser 介面,用於定義Bean的解析邏輯;

解析流程

有關於 Spring 對這部分內容的實現細節,可以參考 Schema解析,下面我對這部分內容做一個簡單的梳理:

  1. Spring 中對Bean的解析主要是通過 DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 方法,具體的解析邏輯委託給 BeanDefinitionParserDelegate 進行;
  2. DefaultBeanDefinitionDocumentReader#parseBeanDefinitions 會區分 默認的Namespace和自定義的Namesapce(除Spring的一些默認標籤外,其它的都是自定義Namespace)
  3. 在解析自定義Namespace的時候會調用 DefaultNamespaceHandlerResolver#resolve 方法, DefaultNamespaceHandlerResolver 中會載入所有 META-INF/spring.handlers 文件裡面的內容,然後維護一套 NamespaceURL => NamespaceHandler 的映射關係。然後在 DefaultNamespaceHandlerResolver#resolve 方法中調用 當前NamespaceURL對應的 NamespaceHandler#init 方法。
  4. dubbo對應的 NamespaceHandler 是 DubboNamespaceHandler,在 DubboNamespaceHandler#init 方法中,會找到各個標籤對應的 BeanDefinitionParser 介面,這裡對應 DubboBeanDefinitionParser 並快取起來;
  5. 在解析標籤的時候會調用 DubboNamespaceHandler#parse 方法,而真正的解析邏輯委託給內部的 DubboBeanDefinitionParser#parse 方法;

補充部分關鍵程式碼:

// DefaultBeanDefinitionDocumentReader#parseBeanDefinitions  protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {      if (delegate.isDefaultNamespace(root)) {          NodeList nl = root.getChildNodes();          for (int i = 0; i < nl.getLength(); i++) {              Node node = nl.item(i);              if (node instanceof Element) {                  Element ele = (Element) node;                  if (delegate.isDefaultNamespace(ele)) {                      // 默認解析                      parseDefaultElement(ele, delegate);                  }else {                      // 自定義解析                      delegate.parseCustomElement(ele);                  }              }          }      }      else {          // 自定義解析          delegate.parseCustomElement(root);      }  }
// DubboNamespaceHandler.java  // NamespaceHandlerSupport是一個抽象類,實現了NamespaceHandler介面  public class DubboNamespaceHandler extends NamespaceHandlerSupport {        static {          Version.checkDuplicate(DubboNamespaceHandler.class);      }        @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("monitor", new DubboBeanDefinitionParser(MonitorConfig.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());      }    }

至此,整個dubbo的xml標籤解析流程就非常清晰了,如果你想通過XML配置的方式來使用dubbo,那麼當你配置好xml之後,隨著 Spring 的啟動,就會自動解析dubbo對應的那些標籤了。

註解啟動

註解是為了讓我們擺脫繁瑣的XML配置,但對程式碼有一定侵入,高版本的dubbo和springboot整合其實非常方便,引入依賴之後只需要在啟動類上添加 @EnableDubbo 註解即可。以 dubbo 2.7.2springboot 2.1.4.RELEASE 為例:

使用示例

依賴

dependencies {      implementation 'org.springframework.boot:spring-boot-starter'      implementation 'org.springframework.boot:spring-boot-starter-web'        implementation 'org.apache.dubbo:dubbo:2.7.2'      implementation 'org.apache.dubbo:dubbo-registry-zookeeper:2.7.2'      implementation 'org.apache.dubbo:dubbo-metadata-report-zookeeper:2.7.2'        implementation 'org.apache.zookeeper:zookeeper:3.4.12'      implementation 'org.apache.curator:curator-recipes:2.12.0'        testImplementation 'org.springframework.boot:spring-boot-starter-test'  }

啟動類

@SpringBootApplication  @EnableDubbo  public class SDubboApplication {        public static void main(String[] args) {          SpringApplication.run(SDubboApplication.class, args);      }        @Configuration      @PropertySource("classpath:/dubbo-provider.properties")      static class ProviderConfiguration {          @Bean          public RegistryConfig registryConfig() {              RegistryConfig registryConfig = new RegistryConfig();              registryConfig.setAddress("zookeeper://10.9.44.133:2181");                // 註冊簡化版的的url到註冊中心              registryConfig.setSimplified(true);              return registryConfig;          }            @Bean          public MetadataReportConfig metadataReportConfig() {              MetadataReportConfig metadataReportConfig = new MetadataReportConfig();              metadataReportConfig.setAddress("zookeeper://10.9.44.133:2181");              return metadataReportConfig;          }            @Bean          public ConfigCenterConfig configCenterConfig() {              ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();              configCenterConfig.setAddress("zookeeper://10.9.44.133:2181");              return configCenterConfig;          }      }    }

dubbo-provider.properties

dubbo.application.name=sdubbo  dubbo.protocol.name=dubbo  dubbo.protocol.port=20882

有關於通過註解定義Provider和Consumer這裡就不介紹了。從上面的程式碼中可以看到,那三個Bean只是一些配置工作,這不是我們關注的重點,重點在 @EnableDubbo 註解,為什麼添加這個註解之後dubbo服務就自動註冊了?

@EnableDubbo

不妨先看看這個註解,可以發現它引用了 @EnableDubboConfig@DubboComponentScan ,前者與配置相關,後者與 服務註冊和服務引用相關。

@Target({ElementType.TYPE})  @Retention(RetentionPolicy.RUNTIME)  @Inherited  @Documented  @EnableDubboConfig  @DubboComponentScan  public @interface EnableDubbo {  ......  }

@EnableDubboConfig

這個註解和 外部化配置相關 ,可以參考一篇博文: 外部化配置

即:一些通用的配置資訊全部配置在 application.properties 或者 bootstrap.properties 配置文件中,dubbo會根據這些配置資訊自動創建 ApplicationConfigRegistryConfigProviderConfig 等Bean,而不需要我們通過註解的方式硬編碼去創建。

其核心原理在 DubboConfigConfigurationRegistrar 類中,這個不是本篇文章的重點,不過多介紹。

其實在上面的示例中,就已經用到了外部化配置特性,雖然沒有在 application.yaml 中定義dubbo的這些屬性,但是在註解類中通過 @PropertySource("classpath:/dubbo-provider.properties") 將這些屬性導入進來了,所以dubbo會自動根據這些屬性去創建相應的Bean, 比如ApplicationConfig,雖然在示例中沒有通過硬編碼的方式創建ApplicationConfig,但是dubbo在讀到 dubbo-provider.properties 文件中的 dubbo.application 屬性時會自動創建一個 ApplicationConfig

外部化配置下,dubbo和springboot整合如下:

依賴

dependencies {      implementation 'org.springframework.boot:spring-boot-starter'      implementation 'org.springframework.boot:spring-boot-starter-web'        implementation 'org.apache.dubbo:dubbo:2.7.2'      implementation 'org.apache.dubbo:dubbo-registry-zookeeper:2.7.2'      implementation 'org.apache.dubbo:dubbo-metadata-report-zookeeper:2.7.2'        implementation 'org.apache.zookeeper:zookeeper:3.4.12'      implementation 'org.apache.curator:curator-recipes:2.12.0'        testImplementation 'org.springframework.boot:spring-boot-starter-test'  }

application.yml

server:    port: 8786  spring:    main:      allow-bean-definition-overriding: true    dubbo:    application:      name: sdubbo    protocol:      name: dubbo      port: 20882    registry:      address: zookeeper://10.9.44.133:2181      simplified: true    metadata-report:      address: zookeeper://10.9.44.133:2181    config-center:      address: zookeeper://10.9.44.133:2181

啟動類

@SpringBootApplication  @EnableDubbo  public class SDubboApplication {      public static void main(String[] args) {          SpringApplication.run(SDubboApplication.class, args);      }  }

@DubboComponentScan

@Target(ElementType.TYPE)  @Retention(RetentionPolicy.RUNTIME)  @Documented  @Import(DubboComponentScanRegistrar.class)  public @interface DubboComponentScan {  ......  }

小擴展

在 Spring 中,通過 @Import 導入一個外部類有三種方式

  1. 直接導入;
@Configuration  @Import(ExternalBean.class)  public class TestImportConfiguration {  }
  1. 導入一個 ImportSelector 介面的實現類,然後重寫 selectImports 方法,在該方法中返回要導入類的全類名;
@Configuration  @Import(TestImportSelect.class)  public class TestImportSelectConfiguration {  }    public class TestImportSelect implements ImportSelector {      @Override      public String[] selectImports(AnnotationMetadata importingClassMetadata) {          return new String[]{"com.sxy.spring.register.ExternalBean"};      }  }
  1. 導入一個 ImportBeanDefinitionRegistrar 介面的實現類,然後重寫 registerBeanDefinitions 方法,在該方法中通過 BeanDefinitionRegistry 註冊 BeanDefinition;
@Configuration  @Import(TestImportBeanDefinitionRegistrar.class)  public class TestImportBeanDefinitionRegistrarCongiguration {  }    public class TestImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {      @Override      public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {          // 註冊一個bean, 指定bean name          registry.registerBeanDefinition("externalBean", new RootBeanDefinition(ExternalBean.class));      }  }

DubboComponentScanRegistrar

DubboComponentScanRegistrar 實現了 ImportBeanDefinitionRegistrar 介面,而在它重寫的 registerBeanDefinitions 方法中做了兩件事:

  1. 註冊 ServiceAnnotationBeanPostProcessor;
  2. 註冊 ReferenceAnnotationBeanPostProcessor;

ServiceAnnotationBeanPostProcessor 實現了 BeanDefinitionRegistryPostProcessor 介面;ReferenceAnnotationBeanPostProcessor 實現了 InstantiationAwareBeanPostProcessorAdapter 介面。 了解Spring的同學都知道這是Spring的擴展介面。

小擴展

  1. BeanFactoryPostProcessor:在實例化bean之前,可以修改BeanDefinition資訊;
  2. BeanDefinitionRegistryPostProcessor: BeanFactoryPostProcessor 介面的子類,在BeanFactoryPostProcessor之前執行,可用於創建 BeanDefinition;
  3. BeanPostProcessor: Bean初始化前後執行。
  4. InstantiationAwareBeanPostProcessor:BeanPostProcessor 的子類,實例化前後執行;
  5. ApplicationContextAwareProcessor:實現了BeanPostProcessor,在postProcessBeforeInitialization中注入各種Aware介面;
ServiceAnnotationBeanPostProcessor

以一個dubbo provider為例

@org.apache.dubbo.config.annotation.Service  public class LeannImpl implements ILearn {      @Override      public String learn(String name) {          return "學習: " + name;      }  }

ServiceAnnotationBeanPostProcessor#postProcessBeanDefinitionRegistry 的方法中,主要做了兩件事:

  1. 根據配置的包掃描路徑找到所有帶有 @org.apache.dubbo.config.annotation.Service 註解的類,為這些類創建 BeanDefinition ,然後註冊到IOC容器中;這部分實現隱藏在 DubboClassPathBeanDefinitionScanner#scan 方法中。
  2. 為每個原始類再創建一個 ServiceBean 類型的 BeanDefinition 資訊。

即:每一個dubbo服務最終會在IOC容器中對應兩個Bean,一個是原始類型,一個是 ServiceBean 類型, ServiceBean 其實是一個 FactoryBean , 是實現服務暴露的關鍵,這裡不展開。 以上面的例子為例,最終兩個Bean對應的BeanName分別為:leannImplServiceBean:com.sxy.sdubbo.service.ILearn

ReferenceAnnotationBeanPostProcessor

以一個dubbo consumer為例

@Component("demoServiceComponent")  public class DemoServiceComponent implements DemoService {      @Reference(timeout = 3000)      private DemoService demoService;        @Override      public String sayHello(String name) {          return demoService.sayHello(name);      }  }

ReferenceAnnotationBeanPostProcessor 主要做了兩件事:

  1. 創建一個代理服務;
  2. DemoServiceComponent 注入值,即 DemoServiceComponent.demoService = 代理服務;

即: DemoServiceComponent 僅僅代表 Spring 容器中的一個普通Bean;而 @Reference註解標註demoService屬性 最終指向的是動態創建的一個代理服務,就是通過這個代理服務實現與provider通訊。

至此,註解模式下,dubbo服務註冊與引用流程已經很清晰了,具體的實現細節可以查看源碼。

自動化配置

自動化配置其實是springboot提供的一個特性,其目的就是盡量讓用戶原理各種繁瑣配置, 其核心原理就是讀取 META-INF/spring.factories 中的自動化配置類,下面簡單介紹一下。

Spring的自動化配置

啟動springboot應用的時候會添加一個 @SpringBootApplication 註解,而該註解中包含了 @EnableAutoConfiguration 註解,而 @EnableAutoConfiguration 就是實現自動化配置的關鍵

@Target(ElementType.TYPE)  @Retention(RetentionPolicy.RUNTIME)  @Documented  @Inherited  @AutoConfigurationPackage  @Import(AutoConfigurationImportSelector.class)  public @interface EnableAutoConfiguration {  ......  }

核心就在 AutoConfigurationImportSelector 類,它實現了 ImportSelector 介面,其實主要就做了一件事情:

  1. 通過 SpringFactoriesLoader#loadFactories 方法載入 classpath 下所有 JAR 文件的 META-INF/spring.factories 文件,然後提取出文件中的所有 xxxEnableAutoConfiguration,這樣就相當於將所有的 xxxEnableAutoConfiguration 註冊到 Spring 容器中了。當然,這些 xxxEnableAutoConfiguration 一般會結合各種 @Conditional 來判斷是否創建Bean。

dubbo-spring-boot-starter

springboot 項目就是由一個個 starter 組成的,一個 starter 通常包含了該模組需要的依賴,通常自動化配置也是在 starter 中完成的。

dubbo在springboot應用中的自動化配置也是通過一個 starter 來完成了,官方Git地址:dubbo-spring-boot-project

那麼,在自動化配置模式先,dubbo與springboot整合應該怎麼做? 可以用外部化配置,也可以用註解的方式來配置,這裡以註解的方式配置為例:

依賴

dependencies {      implementation 'org.springframework.boot:spring-boot-starter'      implementation 'org.springframework.boot:spring-boot-starter-web'        // 因為目前(2019/07/19)dubbo-spring-boot-starter最新只有2.7.1版本      implementation 'org.apache.dubbo:dubbo-spring-boot-starter:2.7.1'      implementation 'org.apache.dubbo:dubbo:2.7.2'      implementation 'org.apache.dubbo:dubbo-registry-zookeeper:2.7.2'      implementation 'org.apache.dubbo:dubbo-metadata-report-zookeeper:2.7.2'        implementation 'org.apache.zookeeper:zookeeper:3.4.12'      implementation 'org.apache.curator:curator-recipes:2.12.0'        testImplementation 'org.springframework.boot:spring-boot-starter-test'  }

啟動類

// @EnableDubbo 不需要加這個註解  @SpringBootApplication  public class SDubboApplication {      public static void main(String[] args) {          SpringApplication.run(SDubboApplication.class, args);      }        @Configuration      @PropertySource("classpath:/dubbo-provider.properties")      static class ProviderConfiguration {          @Bean          public RegistryConfig registryConfig() {              RegistryConfig registryConfig = new RegistryConfig();              registryConfig.setAddress("zookeeper://10.9.44.133:2181");                // 註冊簡化版的的url到註冊中心              registryConfig.setSimplified(true);              return registryConfig;          }          @Bean          public MetadataReportConfig metadataReportConfig() {              MetadataReportConfig metadataReportConfig = new MetadataReportConfig();              metadataReportConfig.setAddress("zookeeper://10.9.44.133:2181");              return metadataReportConfig;          }          @Bean          public ConfigCenterConfig configCenterConfig() {              ConfigCenterConfig configCenterConfig = new ConfigCenterConfig();              configCenterConfig.setAddress("zookeeper://10.9.44.133:2181");              return configCenterConfig;          }      }  }

dubbo-provider.properties

dubbo.application.name=sdubbo  dubbo.protocol.name=dubbo  dubbo.protocol.port=20882  # 多了一個包掃描,在當前 starter 版本中,這個屬性必須配置  dubbo.scan.base-packages =com.sxy.sdubbo

與註解方式的區別:

  1. 添加 dubbo-spring-boot-starter 依賴;
  2. 不需要配置 @EnableDubbo 註解;
  3. 需要配置包掃描路徑 dubbo.scan.base-packages

其實現原理就是springboot中的自動化配置: dubbo-spring-boot-starter 的pom.xml文件中引入了 dubbo-spring-boot-autoconfigure 模組, dubbo-spring-boot-autoconfigure 的pom.xml文件中引入了 dubbo-spring-boot-autoconfigure-compatible 模組;

  1. dubbo-spring-boot-autoconfigure 模組中,META-INF/spring.factories 文件內容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=  org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration
  1. dubbo-spring-boot-autoconfigure-compatible 模組中,META-INF/spring.factories 文件內容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=  org.apache.dubbo.spring.boot.autoconfigure.DubboAutoConfiguration,  org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBindingAutoConfiguration  org.springframework.context.ApplicationListener=  org.apache.dubbo.spring.boot.context.event.OverrideDubboConfigApplicationListener,  org.apache.dubbo.spring.boot.context.event.WelcomeLogoApplicationListener,  org.apache.dubbo.spring.boot.context.event.AwaitingNonWebApplicationListener  org.springframework.boot.env.EnvironmentPostProcessor=  org.apache.dubbo.spring.boot.env.DubboDefaultPropertiesEnvironmentPostProcessor  org.springframework.context.ApplicationContextInitializer=  org.apache.dubbo.spring.boot.context.DubboApplicationContextInitializer

DubboRelaxedBindingAutoConfiguration 主要是和屬性解析有關,這裡不做介紹;核心還是 DubboAutoConfiguration , 在該類中有一下兩段關鍵程式碼

/**   * @ConditionalOnProperty(prefix = DUBBO_SCAN_PREFIX, name = BASE_PACKAGES_PROPERTY_NAME) 強制要求了我們需要配置包掃描路徑,否則該Bean不會被創建   * Creates {@link ServiceAnnotationBeanPostProcessor} Bean   *   * @param propertyResolver {@link PropertyResolver} Bean   * @return {@link ServiceAnnotationBeanPostProcessor}   */  @ConditionalOnProperty(prefix = DUBBO_SCAN_PREFIX, name = BASE_PACKAGES_PROPERTY_NAME)  @ConditionalOnBean(name = BASE_PACKAGES_PROPERTY_RESOLVER_BEAN_NAME)  @Bean  public ServiceAnnotationBeanPostProcessor serviceAnnotationBeanPostProcessor(          @Qualifier(BASE_PACKAGES_PROPERTY_RESOLVER_BEAN_NAME) PropertyResolver propertyResolver) {      Set<String> packagesToScan = propertyResolver.getProperty(BASE_PACKAGES_PROPERTY_NAME, Set.class, emptySet());      return new ServiceAnnotationBeanPostProcessor(packagesToScan);  }    /**   * Creates {@link ReferenceAnnotationBeanPostProcessor} Bean if Absent bean工廠不存在referenceAnnotationBeanPostProcessor時創建   *   * @return {@link ReferenceAnnotationBeanPostProcessor}   */  @ConditionalOnMissingBean  @Bean(name = ReferenceAnnotationBeanPostProcessor.BEAN_NAME)  public ReferenceAnnotationBeanPostProcessor referenceAnnotationBeanPostProcessor() {      return new ReferenceAnnotationBeanPostProcessor();  }

其實核心就是通過自動化的方式創建了 ServiceAnnotationBeanPostProcessorReferenceAnnotationBeanPostProcessor,沒有通過 @EnableDubbo 註解觸發。但是感覺這樣不太好用。