天天看點

SOFABoot源碼解析之子產品化開發

1.SOFABoot源碼解析

1.1  子產品化開發

1.1.1 簡述

        關于SOFABoot子產品化開發的文檔來自于Alipay開源社群Wiki,連結位址為:

        https://github.com/alipay/sofa-boot/wiki/Modular-Development

        此處直接引用該文檔内容,主要是便于大家了解源碼解析部分。

1.1.1.1  子產品化開發簡述

        SOFABoot從 2.4.0 版本開始支援基于 Spring 上下文隔離的子產品化開發能力。為了更好的了解 SOFABoot 子產品化開發的概念,我們來區分幾個常見的子產品化形式:

        1).  基于代碼組織上的子產品化:這是最常見的形式,在開發期,将不同功能的代碼放在不同 Java 工程下,在編譯期被打進不同 jar 包,在運作期,所有 Java 類都在一個 classpath 下,沒做任何隔離;

        2).  基于 Spring 上下文隔離的子產品化:借用 Spring 下文來做不同功能子產品的隔離,在開發期和編譯期,代碼和配置也會分在不同Java 工程中,但在運作期,不同的 Spring Bean 互相不可見,IoC 隻在同一個上下文内部發生,但是所有的 Java 類還是在一個 ClassLoader 下;

        3).  基于 ClassLoader 隔離的子產品化:借用 ClassLoader 來做隔離,每個子產品都有獨立的 ClassLoader,子產品與子產品之間的 classpath 不同,SOFAArk 就是這種子產品化的實踐方式。

        SOFABoot子產品化開發屬于第二種子產品化形式,基于 Spring 上下文隔離的子產品化。每個 SOFABoot 子產品使用獨立的 Spring 上下文,避免不同 SOFABoot 子產品間的 BeanId 沖突,有效降低企業級多子產品開發時團隊間的溝通成本。

SOFABoot源碼解析之子產品化開發

        1.  SOFABoot子產品

        SOFABoot架構定義了 SOFABoot 子產品的概念,一個 SOFABoot 子產品是一個包括 Java 代碼、Spring 配置檔案、SOFABoot 子產品辨別等資訊的普通 Jar 包,一個 SOFABoot 應用可以包含多個 SOFABoot 子產品,每個 SOFABoot 子產品都含有獨立的 Spring 上下文。

        以 SOFABoot子產品為單元的子產品化方式為開發者提供了以下功能:

        1)  運作時,每個SOFABoot子產品的 Spring 上下文是隔離的,子產品間定義的 Bean 不會互相影響。

        2)  每個 SOFABoot 子產品是功能完備且自包含的,可以很容易在不同的 SOFABoot 應用中進行子產品遷移和複用,隻需将 SOFABoot 子產品整個拷貝過去,調整 Maven 依賴,即可運作。

        2.  SOFABoot子產品間通信

        上下文隔離後,子產品與子產品間的 Bean 無法直接注入,子產品間需要通過 SOFA 服務進行通信,目前SOFABoot 提供了兩種形式的服務釋出和引用,用于解決不同級别的子產品間調用的問題:

        1)  JVM 服務釋出和引用:解決一個 SOFABoot 應用内部各個SOFABoot 子產品之間的調用問題。

        2)  RPC 服務釋出和引用:解決多個 SOFABoot 應用之間的遠端調用問題,RPC服務釋出與引用。

        3.  子產品并行化啟動

        每個 SOFABoot 子產品都是獨立的 Spring 上下文,多個 SOFABoot 子產品支援并行化啟動,與 Spring Boot 的單 Spring 上下文模式相比,子產品并行化啟動能夠加快應用的啟動速度。

        4.  RootApplication Context

        SOFABoot應用運作時,本身會産生一個Spring Context,我們把它叫做 Root Application Context,它是每個 SOFABoot 子產品建立的 Spring Context 的 Parent。這樣設計的目的是為了保證每個 SOFABoot 子產品的 Spring Context 都能發現 Root ApplicationContext 中建立的 Bean,這樣當應用新增Starter 時,不僅 Root Application Context 能夠使用 Starter 中新增的 Bean,每個 SOFABoot 子產品的 Spring Context 也能使用這些 Bean。

1.1.1.2  SOFABoot子產品簡述

        SOFABoot 子產品是一個普通的 Jar 包加上一些 SOFABoot 特有的配置,這些 SOFABoot 特有的配置,讓一個 Jar 包能夠被 SOFABoot 識别,使之具備子產品化的能力。

        一個完整的 SOFABoot 子產品和一個普通的 Jar 包有兩點差別:

        1) SOFABoot 子產品包含一份 sofa-module.properties 檔案,這份檔案裡面定義了 SOFABoot子產品的名稱以及子產品之間的依賴關系。

        2) SOFABoot 子產品的 META-INF/spring 目錄下,可以放置任意多的 Spring 配置檔案,SOFABoot 會自動把它們作為本子產品的 Spring 配置加載起來。

        1.  sofa-module.properties 檔案詳解

        先來看一份完整的 sofa-module.properties 檔案:

1.  Module-Name=com.alipay.test.biz.service.impl

2.  Spring-Parent=com.alipay.test.common.dal

3.  Require-Module=com.alipay.test.biz.shared

4.  Module-Profile=dev

        1) Module-Name

        Module-Name 是SOFABoot 子產品的名稱,也是 SOFABoot 子產品的唯一标示符。在一個 SOFABoot 應用中,一個 SOFABoot 子產品的 Module-Name 必須和其他的 SOFABoot 子產品的 Module-Name 不一樣。需要注意的一點是,一個 SOFABoot 應用運作時的 SOFABoot 子產品,不僅僅隻包含本應用的子產品,還包括依賴了其他應用的SOFABoot 子產品,确定是否唯一的時候需要把這些 SOFABoot 子產品也考慮進去。

        2) Require-Module

        Require-Module 用于定義子產品之間的依賴順序,值是以逗号分隔的 SOFABoot 子產品名清單,比如上面的配置中,就表示本子產品依賴于com.alipay.test.biz.shared 子產品。對于這種依賴關系的處理,SOFABoot 會将 com.alipay.test.biz.shared 子產品在本子產品之前啟動,即com.alipay.test.biz.shared子產品将先啟動 Spring 上下文。

        一般情況下,是不需要為子產品定義 Require-Module 的,隻有當子產品的 Spring 上下文的啟動依賴于另一個子產品的 Spring 上下文的啟動時,才需要定義 Require-Module。舉一個例子,如果你在 A 子產品中釋出了一個 SOFA JVM Service。在 B 子產品的某一個 Bean 的 init 方法裡面,需要使用 SOFA Reference 調用這個 JVM Service。假設 B 子產品在 A 子產品之前啟動了,那麼B 子產品的 Bean 就會因為 A 子產品的 JVM Service 沒有釋出而 init 失敗,導緻 Spring 上下文啟動失敗。這個時候,我們就可以使用 Require-Module 來強制 A 子產品在 B 子產品之前啟動。

        3) Spring-Parent

        在 SOFABoot 應用中,每一個 SOFABoot 子產品都是一個獨立的 Spring 上下文,并且這些 Spring 上下文之間是互相隔離的。雖然這樣的子產品化方式可以帶來諸多好處,但是,在某些場景下還是會有一些不便,這個時候,你可以通過 Spring-Parent 來打通兩個 SOFABoot 子產品的 Spring 上下文。Spring-Parent 屬性可以配置一個子產品的名稱,比如上面的配置中,就将 com.alipay.test.common.dal 的 Spring 上下文設定為目前子產品的 Spring 上下文的父 Spring 上下文。

        由于 Spring 的限制,一個子產品的 Spring-Parent 隻能有一個子產品。

        關于 Spring 的父上下文的作用可以看 Spring 的 BeanFactory 的說明:http://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/beans/factory/BeanFactory.html

        4) Module-Profile

        支援 SOFABoot Profile 能力。

1.1.1.3  子產品并發啟動

        SOFABoot 會根據Require-Module 計算子產品依賴樹,例如以下依賴樹表示子產品B 和子產品C 依賴子產品A,子產品E 依賴子產品D,子產品F 依賴子產品E:

SOFABoot源碼解析之子產品化開發

        該依賴樹會保證子產品A 必定在子產品B 和子產品C 之前啟動,子產品D 在子產品E 之前啟動,子產品E 在子產品F 之前啟動,但是依賴樹沒有定義子產品B 與子產品C,子產品B、C與子產品D、E、F之間的啟動順序,這幾個子產品之間可以串行啟動,也可以并行啟動。

        SOFABoot 預設會并行啟動子產品,在使用過程中,如果希望關閉并行啟動,可以在 application.properties 中增加以下參數:

1.  com.alipay.sofa.boot.module-start-up-parallel=false

 1.1.1.4  SOFABoot Profile

        Spring 架構從3.1.X 版本開始提供了 profile 功能: BeanDefinition Profiles,SOFABoot 支援子產品級 profile 能力,即在各個子產品啟動的時候決定子產品是否能夠啟動。

        1.   使用Module-Profile激活module

        使用 SOFABoot 的profile 功能,需要在 application.properties 檔案增加 com.alipay.sofa.boot.active-profiles 字段,該字段的值為逗号分隔的字元串,表示允許激活的 profile 清單,指定該字段後,SOFABoot 會為每個可以激活的子產品指定此字段表示的 profile 清單。

        SOFABoot 子產品的sofa-module.properties 檔案支援 Module-Profile 字段,該字段的值為逗号分隔的字元串,表示目前子產品允許在哪些 profile 激活。Module-Profile 支援取反操作, !dev 表示com.alipay.sofa.boot.active-profiles 不包含 dev 時被激活。

        當應用未指定com.alipay.sofa.boot.active-profiles 參數時,表示應用所有子產品均可啟動。SOFABoot子產品未指定 Module-Profile 時,表示目前SOFABoot 子產品可以在任何 profile 啟動。

        2.   使用例子

        1) 激活 dev SOFABoot 子產品

        application.properties 中增加配置如下:

1.  com.alipay.sofa.boot.active-profiles=dev

        該配置表示激活 profile 為 dev 的子產品。

        在每個需要限定為 dev profile 被激活子產品的 sofa-module.properties 檔案中增加如下配置:

1.  Module-Profile=dev

        2) 配置多個激活 profile

        application.properties 中增加配置如下:

1.  com.alipay.sofa.boot.active-profiles=dev,test

        該配置表示激活 profile 為 dev 或者 test 的子產品。

        在 SOFABoot 子產品的 sofa-module.properties 檔案中增加如下配置:

1.  Module-Profile=test,product

        該配置表示當 com.alipay.sofa.boot.active-profiles包含 test 或者 product 時激活子產品,由于目前指定的 com.alipay.sofa.boot.active-profiles 為dev,test ,此子產品将被激活。

        3) Module-Profile 取反

        application.properties 中增加配置如下:

1.  com.alipay.sofa.boot.active-profiles=dev

        該配置表示激活 profile 為 dev 的子產品。

        在 SOFABoot 子產品的 sofa-module.properties 檔案中增加如下配置:

1.  Module-Profile=!product

        該配置表示當com.alipay.sofa.boot.active-profiles 不包含 product 時激活子產品,由于目前指定的 com.alipay.sofa.boot.active-profiles 為 dev ,此子產品将被激活。

        4) 設定激活子產品 Spring 上下文的 spring.profiles.active 屬性

        application.properties 中增加配置如下:

1.  com.alipay.sofa.boot.active-profiles=dev,test

        該配置表示激活 profile 為 dev 或者 test 的子產品,當一個子產品滿足上面的激活條件時,這個子產品就會被啟動,同時 Spring 上下文的環境資訊 spring.profiles.active 也被設定為了 dev,test ,這樣如下的配置 beanId 為 devBeanId 和 testBeanId 的bean都會被激活。

1.  <?xml version="1.0"encoding="UTF-8"?>

2.  <beansxmlns="http://www.springframework.org/schema/beans"

3.         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

4.         xmlns:jdbc="http://www.springframework.org/schema/jdbc"

5.         xmlns:jee="http://www.springframework.org/schema/jee"

6.         xsi:schemaLocation="http://www.springframework.org/schema/beans

7.                                                  http://www.springframework.org/schema/beans/spring-beans-3.2.xsd

8.                                                  http://www.springframework.org/schema/context

9.                                                  http://www.springframework.org/schema/context/spring-context.xsd"

10.        default-autowire="byName">      

11.  

12.     <beans profile="dev">

13.         <bean id="devBeanId"class="com.alipay.cloudenginetest.sofaprofilesenv.DemoBean">

14.             <propertyname="name">

15.                 <value>demoBeanDev</value>

16.             </property>

17.         </bean>

18.     </beans>

19.  

20.     <beans profile="test">

21.         <bean id="testBeanId"class="com.alipay.cloudenginetest.sofaprofilesenv.DemoBean">

22.             <propertyname="name">

23.                 <value>demoBeanTest</value>

24.             </property>

25.         </bean>

26.     </beans>

27. </beans>

1.1.2 源碼解析

        以SOFABoot自帶的子產品化案例sofaboot-sample-with-isle為例,較長的描述SOFABoot子產品化啟動原理。

        在此提前說明,源碼分析主要分析主流程,以及本人認為比較重要的一些内容,對于其它部分,大家可以基于本文檔,自行研讀。

        sofaboot-sample-with-isle項目主要包括:

        1.  sofa-boot-run:主子產品;

        2.  service-facade:服務接口子產品

        3.  service-provider:服務提供者子產品;

        4.  service-consumer:服務消費者子產品;

        由于項目基于SpringBoot架構開發,是以運作主子產品sofa-boot-run中ApplicationRun類main方法,通過SpringApplication.run方法啟動應用。

1.  @SpringBootApplication

2.  public class ApplicationRun {

3.      public static void main(String[] args) {

4.          SpringApplication.run(ApplicationRun.class,args);

5.      }

6.  }

        重新整理Spring應用上下文時,在invokeBeanFactoryPostProcessors(beanFactory)方法執行過程中,自動加載和注冊SOFABoot的isle-sofa-boot-starter子產品中META-INF/spring.factories檔案中配置的自動配置相關的類:

1.  org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

2.  com.alipay.sofa.isle.spring.configuration.SofaModuleAutoConfiguration

        在SofaModuleAutoConfiguration類中,主要定義了以下幾個自動配置的類:

        1.  SofaModuleProperties

        2.  SofaModuleBeanFactoryPostProcessor

        3.  SofaModuleContextRefreshedListener

        4.  SofaModuleHealthIndicator

        5.  SofaModuleHealthChecker

        其中,SofaModuleContextRefreshedListener類監聽根Spring應用上下文(AnnotationConfigEmbeddedWebApplicationContext)重新整理完成以後釋出的ContextRefreshedEvent事件,進行子子產品的加載,後面詳述。

        Spring應用上下文重新整理過程中的其它步驟基本與SOFABoot的啟動執行過程相同,詳細說明參考《啟動原理部分》。

        在finishRefresh()方法執行過程中,釋出ContextRefreshedEvent事件,表示根Spring應用上下文重新整理完成。

1.  protectedvoid finishRefresh() {

2.       super.finishRefresh();

3.       EmbeddedServletContainerlocalContainer = startEmbeddedServletContainer();

4.       if(localContainer != null) {

5.           publishEvent(

6.                   newEmbeddedServletContainerInitializedEvent(this, localContainer));

7.       }

8.  }

        調用AbstractApplicationContext的finishRefresh方法釋出ContextRefreshedEvent事件:

1.  protected void finishRefresh() {

2.        ……略

3.       //Publish the final event.

4.       publishEvent(new ContextRefreshedEvent(this));

5.   

6.       ……略

7.  }

        SofaModuleContextRefreshedListener執行個體監聽到ContextRefreshedEvent事件,開始執行子子產品的加載:

1.  public voidonApplicationEvent(ContextRefreshedEvent event) {

2.          if (INIT.compareAndSet(false, true)) {

3.              DefaultPipelineContext pipelineContext = newDefaultPipelineContext();

4.              pipelineContext.appendStage(newModelCreatingStage((AbstractApplicationContext) event

5.                  .getApplicationContext()));

6.              pipelineContext.appendStage(newSpringContextInstallStage(

7.                  (AbstractApplicationContext)event.getApplicationContext()));

8.              pipelineContext.appendStage(newModuleLogOutputStage((AbstractApplicationContext) event

9.                  .getApplicationContext()));

10.             try {

11.                 pipelineContext.process();

12.             } catch (Throwable t) {

13.                 SofaLogger.error(t,"process pipeline error");

14.                 throw new RuntimeException(t);

15.             }

16.         }

17.     }

        通過原子操作CAS設定全局變量INIT為true,保證隻有一個線程在執行子子產品的加載工作,并且隻執行一次。

        建立DefaultPipelineContext執行個體,含有一個有序的ArrayList的屬性,用于存儲多個PipelineStage接口的實作,每個PipelineStage代表一個階段,多個PipelineStage可以按照增加到數組清單中的順序依次執行。

        建立ModelCreatingStage執行個體,參數為根Spring應用上下文AnnotationConfigEmbeddedWebApplicationContext,并增加到DefaultPipelineContext的ArrayList中。

        建立SpringContextInstallStage執行個體,參數為根Spring應用上下文AnnotationConfigEmbeddedWebApplicationContext,并增加到DefaultPipelineContext的ArrayList中。

        建立ModuleLogOutputStage執行個體,參數為根Spring應用上下文AnnotationConfigEmbeddedWebApplicationContext,并增加到DefaultPipelineContext的ArrayList中。

        調用DefaultPipelineContext的process方法,按照進入數組清單的順序依次執行每個PipelineStage的process方法。

        一、  執行ModelCreatingStage的process方法:

1.      protected void doProcess() throws Exception{

2.          ApplicationRuntimeModel application = newApplicationRuntimeModel();

3.          application.setAppName(appName);

4.          application.setModuleDeploymentValidator(newDefaultModuleDeploymentValidator());

5.          getAllDeployments(application);

6.          applicationContext.getBeanFactory().registerSingleton(

7.              SofaModuleFrameworkConstants.APPLICATION,application);

8.      }

        建立ApplicationRuntimeModel執行個體application,該執行個體存儲應用所有的子產品部署資訊。

        然後調用getAllDeployments方法加載所有的SOFABoot子產品:

1.  private voidgetAllDeployments(ApplicationRuntimeModel application) throws IOException {

2.          Enumeration<URL> urls = appClassLoader

3.              .getResources(SofaModuleFrameworkConstants.SOFA_MODULE_FILE);

4.          if (urls == null ||!urls.hasMoreElements())

5.              return;

6.   

7.          while (urls.hasMoreElements()) {

8.              URL url = urls.nextElement();

9.              UrlResource urlResource = newUrlResource(url);

10.             Properties props = newProperties();

11.             props.load(urlResource.getInputStream());

12.             DeploymentDescriptorConfigurationdeploymentDescriptorConfiguration = new DeploymentDescriptorConfiguration(

13.                 Collections.singletonList(SofaModuleFrameworkConstants.MODULE_NAME),

14.                 Collections.singletonList(SofaModuleFrameworkConstants.REQUIRE_MODULE));

15.             DeploymentDescriptor dd =DeploymentBuilder.build(url, props,

16.                 deploymentDescriptorConfiguration,appClassLoader);

17.  

18.             if (application.isModuleDeployment(dd)) {

19.                 if(SofaModuleProfileUtil.acceptProfile(applicationContext, dd)) {

20.                     application.addDeployment(dd);

21.                 } else {

22.                     application.addInactiveDeployment(dd);

23.                 }

24.             }

25.         }

26.     }

        使用應用的類加載器加載classpath路徑上的sofa-module.properties檔案(涉及項目本身及所依賴的Jar包)。此時,類加載器為sun.misc.Launcher$AppClassLoader的一個執行個體。在此案例中,主要包括兩個sofa-module.properties檔案。出于調試目的,本項目采用項目依賴方式,而非Jar包依賴,是以sofa-module.properties檔案分别位于service-provider和service-consumer項目的編譯輸出目錄中,如file:/D:/SOFA_Workspace/sofa-boot-master/sofaboot-samples/sofaboot-sample-with-isle/service-provider/target/classes/sofa-module.properties(如果采用Jar包依賴方式,則位于service-provider和service-consumer的Jar包)。

        根據每個子產品的sofa-module.properties檔案,依次為每個子產品建立子產品的部署描述執行個體FileDeploymentDescriptor(如果采用Jar包模式部署,則建立JarDeploymentDescriptor),基本過程如下:

        1.   建立sofa-module.properties檔案對應的UrlResource執行個體;

        2.   讀取sofa-module.properties檔案内容,置入Properties執行個體;

        3.   建立DeploymentDescriptorConfiguration執行個體;

        4.   通過DeploymentBuilder的build方法建立SOFABoot子產品的DeploymentDescriptor執行個體dd;

        5.   如果dd的Module-Name為空,則處理下一個子產品的sofa-module.properties檔案;

        6.   如果dd的Module-Name不為空,并且本子產品的profile 在項目允許激活的profile清單内(本子產品未指定 Module-Profile 時,表示目前本子產品可以在任何 profile 啟動),則把dd增加到application的deploys數組清單、deployRegistry注冊中心、springPowered。否則,把dd增加到application的inactiveDeploys數組清單。

        處理完所有SOFABoot子產品的配置檔案以後,以單例模式把application執行個體注冊到根Spring應用上下文,名字為SOFABOOT-APPLICATION。

        二、  執行SpringContextInstallStage的process方法:

1.      protected void doProcess() throws Exception{

2.          ApplicationRuntimeModel application =applicationContext.getBean(

3.              SofaModuleFrameworkConstants.APPLICATION,ApplicationRuntimeModel.class);

4.   

5.          try {

6.              doProcess(application);

7.          } catch (Throwable e) {

8.              ……略

9.          }

10.     }

        從根Spring應用上下文中擷取名字為SOFABOOT-APPLICATION的ApplicationRuntimeModel執行個體application,然後處理該application:

1.  private voiddoProcess(ApplicationRuntimeModel application) throws Exception {

2.          outputModulesMessage(application);

3.          SpringContextLoader springContextLoader= new DynamicSpringContextLoader(applicationContext);

4.          installSpringContext(application, springContextLoader);

5.   

6.          if(applicationContext.getBean(SofaModuleFrameworkConstants.SOFA_MODULE_PROPERTIES_BEAN_ID,

7.              SofaModuleProperties.class).isModuleStartUpParallel()){

8.              refreshSpringContextParallel(application);

9.          } else {

10.             refreshSpringContext(application);

11.         }

12.     }

        (一)    輸出SOFABoot子產品資訊:

1.  private voidoutputModulesMessage(ApplicationRuntimeModel application)

2.                                                                            throwsDeploymentException {

3.          StringBuilder stringBuilder = newStringBuilder();

4.          ……略

5.          writeMessageToStringBuilder(stringBuilder,application.getResolvedDeployments(),

6.              "Modules that couldinstall");      

7.          ……略

8.  }

        此處,調用application的getResolvedDeployments()方法,解析并擷取所有子產品對應的DeploymentDescriptor執行個體:

1.      public List<DeploymentDescriptor>getResolvedDeployments() {

2.          ……略

3.          resolvedDeployments = deployRegistry.getResolvedObjects();

4.          ……略

5.          return resolvedDeployments;

6.      }

        調用DeployRegistry的getResolvedObjects方法,擷取所有在SOFABoot子產品建立階段時增加到application中的DeploymentDescriptor。

1.      public List<DeploymentDescriptor>getResolvedObjects() {

2.          if (!deployments.isEmpty()) {

3.              commitDeployments();

4.          }

5.          return super.getResolvedObjects();

6.      }

        調用commitDeployments方法:

1.      private void commitDeployments() {

2.          for (DeploymentDescriptor fd :deployments.values()) {

3.              add(fd.getModuleName(), fd,fd.getRequiredModules());

4.          }

5.          deployments.clear();

6.      }

        依次周遊所有子產品對應的DeploymentDescriptor,以子產品命名、子產品執行個體、子產品依賴的子產品集合為參數,調用add方法,在Map中注冊DeploymentDescriptor,并更新DeploymentDescriptor的依賴關系。

1.  public void add(K key, T object,Collection<K> requires) {

2.          Entry<K, T> entry =registry.get(key);

3.          if (entry == null) {

4.              entry = new Entry<>(key,object);

5.              registry.put(key, entry);

6.          } else if (entry.object == null) {

7.              entry.object = object;

8.          } else {

9.              return;

10.         }

11.         updateDependencies(entry, requires);

12.         // resolve it if no pendingrequirements

13.         if (entry.isResolved()) {

14.             resolve(entry);

15.         }

16.     }

        更新子產品的依賴性:

1.  protected voidupdateDependencies(Entry<K, T> entry, Collection<K> requires) {

2.          if (requires != null) {

3.              for (K req : requires) {

4.                  Entry<K, T> reqEntry =registry.get(req);

5.                  if (reqEntry != null) {

6.                      if (reqEntry.isResolved()){

7.                          // requirementsatisfied -> continue

8.                          reqEntry.addDependsOnMe(entry);

9.                          entry.addDependency(reqEntry);

10.                         continue;

11.                     }

12.                 } else {

13.                     reqEntry = newEntry<>(req, null);

14.                     registry.put(req,reqEntry);

15.                 }

16.                 // dependencies not satisfied

17.                 reqEntry.addDependsOnMe(entry);

18.                 entry.addDependency(reqEntry);

19.                 entry.addWaitingFor(reqEntry);

20.             }

21.         }

22.     }

        在此,根據sofa-module.properties檔案中Require-Module屬性,設定子產品的依賴屬性,主要包括本子產品所依賴的其它子產品、依賴與本子產品的其它子產品等。其中,本子產品所依賴的子產品的集合在并行重新整理子產品的Spring應用上下文時,被作為子產品是否具備重新整理的條件(如果dependencies集合為空或size為0,則表示本子產品不依賴其它子產品,或所依賴的子產品已經重新整理完成,本子產品具體重新整理條件。否則,等待所依賴的子產品重新整理完成)。

        (二)    以根Spring應用上下文作為參數,建立DynamicSpringContextLoader執行個體;

        (三)    調用installSpringContext方法建立Spring應用上下文:

1.  private voidinstallSpringContext(ApplicationRuntimeModel application,

2.                                        SpringContextLoaderspringContextLoader) {

3.          ClassLoader oldClassLoader =Thread.currentThread().getContextClassLoader();

4.   

5.          for (DeploymentDescriptor deployment :application.getResolvedDeployments()) {

6.              if(deployment.isSpringPowered()) {

7.                  ……略

8.                  try {

9.                      Thread.currentThread().setContextClassLoader(deployment.getClassLoader());

10.                     springContextLoader.loadSpringContext(deployment,application);

11.                 } catch (Throwable e) {

12.                     SofaLogger.error(e, "Install module{0} got an error!", deployment.getName());

13.                     application.addFailed(deployment);

14.                 } finally {

15.                     Thread.currentThread().setContextClassLoader(oldClassLoader);

16.                 }

17.             }

18.         }

19.     }

        首先,儲存目前線程的上下文類加載器,此時為sun.misc.Launcher$AppClassLoader執行個體。然後,周遊application中加載的每個子產品的DeploymentDescriptor執行個體,并根據DeploymentDescriptor執行個體依次為每個SOFABoot子產品建立Spring應用上下文。

        針對每個DeploymentDescriptor執行個體,主要處理邏輯如下:

        1) 判斷SOFABoot子產品是否為基于Spring的子產品,判斷條件為SOFABoot 子產品的META-INF\spring目錄下是否存在Spring的XML配置檔案,如D:\SOFA_Workspace\sofa-boot-master\sofaboot-samples\sofaboot-sample-with-isle\service-provider\target\classes\META-INF\spring目錄。如果springResources為null,則調用loadSpringXMLs方法,把META-INF/目錄下的所有SpringXML配置檔案增加到springResources中,待後面加載子產品的Spring應用上下文時,把springResources中所有SpringXML配置檔案中的Bean注冊到Spring上下文。初始化完springResources,判斷springResources是否為空,如果為空,則表示此子產品不是Spring子產品,直接跳過處理下一個SOFABoot 子產品。如果不為空,則表示此子產品為Spring子產品,繼續處理:

        2) 設定目前線程的上下文類加載器為DeploymentDescriptor中的類加載器,此處為sun.misc.Launcher$AppClassLoader執行個體。

        3) 調用DynamicSpringContextLoader的loadSpringContext方法,建立SOFABoot 子產品對應的Spring應用上下文。

1.  public voidloadSpringContext(DeploymentDescriptor deployment,

2.                                    ApplicationRuntimeModelapplication) throws Exception {

3.          SofaModulePropertiessofaModuleProperties = rootApplicationContext

4.              .getBean(SofaModuleFrameworkConstants.SOFA_MODULE_PROPERTIES_BEAN_ID,

5.                  SofaModuleProperties.class);

6.          BeanLoadCostBeanFactory beanFactory =new BeanLoadCostBeanFactory(

7.              sofaModuleProperties.getBeanLoadCost());

8.          beanFactory.setParameterNameDiscoverer(newLocalVariableTableParameterNameDiscoverer());

9.          beanFactory

10.             .setAutowireCandidateResolver(newQualifierAnnotationAutowireCandidateResolver());

11.  

12.         GenericApplicationContext ctx =sofaModuleProperties.isPublishEventToParent() ? new GenericApplicationContext(

13.             beanFactory) : newSofaModuleApplicationContext(beanFactory);

14.         String activeProfiles =sofaModuleProperties.getActiveProfiles();

15.         if(StringUtils.hasText(activeProfiles)) {

16.             String[] profiles = activeProfiles

17.                 .split(SofaModuleFrameworkConstants.PROFILE_SEPARATOR);

18.             ctx.getEnvironment().setActiveProfiles(profiles);

19.         }

20.         setUpParentSpringContext(ctx,deployment, application);

21.         final ClassLoader moduleClassLoader =deployment.getClassLoader();

22.         ctx.setClassLoader(moduleClassLoader);

23.         CachedIntrospectionResults.acceptClassLoader(moduleClassLoader);

24.  

25.         // set allowBeanDefinitionOverriding

26.         ctx.setAllowBeanDefinitionOverriding(rootApplicationContext.getBean(

27.             SofaModuleProperties.class).isAllowBeanDefinitionOverriding());

28.  

29.         ctx.getBeanFactory().setBeanClassLoader(moduleClassLoader);

30.         ctx.getBeanFactory().addPropertyEditorRegistrar(newPropertyEditorRegistrar() {

31.  

32.             public voidregisterCustomEditors(PropertyEditorRegistry registry) {

33.                 registry.registerCustomEditor(Class.class,new ClassEditor(moduleClassLoader));

34.                 registry.registerCustomEditor(Class[].class,

35.                     newClassArrayEditor(moduleClassLoader));

36.             }

37.         });

38.         deployment.setApplicationContext(ctx);

39.  

40.         XmlBeanDefinitionReader beanDefinitionReader =new XmlBeanDefinitionReader(ctx);

41.         beanDefinitionReader.setValidating(true);

42.         beanDefinitionReader.setNamespaceAware(true);

43.         beanDefinitionReader

44.             .setBeanClassLoader(deployment.getApplicationContext().getClassLoader());

45.         beanDefinitionReader.setResourceLoader(ctx);

46.  

47.         for (Map.Entry<String, Resource>entry : deployment.getSpringResources().entrySet()) {

48.             String fileName = entry.getKey();

49.             beanDefinitionReader.loadBeanDefinitions(entry.getValue());

50.             deployment.addInstalledSpringXml(fileName);

51.         }

52.         addPostProcessors(beanFactory);

53.     }

        loadSpringContext方法主要處理邏輯如下:

        1) 從根Spring應用上下文AnnotationConfigEmbeddedWebApplicationContext中擷取名為sofaModuleProperties的SofaModuleProperties執行個體。

        2) 建立BeanLoadCostBeanFactory執行個體,此類繼承DefaultListableBeanFactory,增加了記錄建立指定Bean時間的功能。可以設定Bean加載的下限時限,預設100MS,建立Bean的時間超過這個時限時,則把Bean增加到beanCostList數組清單中,供後續使用。

        3) 設定BeanLoadCostBeanFactory執行個體的屬性,主要包括:setParameterNameDiscoverer、setAutowireCandidateResolver。

        4) 根據SofaModuleProperties執行個體中publishEventToParent屬性值,建立不同類型的ApplicationContext。如果publishEventToParent為true,表示事件需要釋出給父Spring應用上下文,則建立org.springframework.context.support.GenericApplicationContext.GenericApplicationContext(DefaultListableBeanFactorybeanFactory)執行個體。否則,建立com.alipay.sofa.isle.spring.context.SofaModuleApplicationContext.SofaModuleApplicationContext(DefaultListableBeanFactorybeanFactory)執行個體。此處為false,是以建立SofaModuleApplicationContext執行個體。

        5) 擷取SofaModuleProperties執行個體中activeProfiles屬性值,如果不為空,則SofaModuleApplicationContext執行個體中Enviroment屬性的ActiveProfiles。

        6) 設定SofaModuleApplicationContext的父Spring應用上下文。如果sofa-module.properties檔案設定了Spring-Parent屬性,則父Spring應用上下文為Spring-Parent屬性指定的SOFABoot子產品的Spring應用上下文。如果沒有設定Spring-Parent屬性,則父Spring應用上下文為根Spring應用上下文AnnotationConfigEmbeddedWebApplicationContext。

        7) 設定SofaModuleApplicationContext類加載器為DeploymentDescriptor中的類加載器。

        8) 設定SofaModuleApplicationContext類的屬性allowBeanDefinitionOverriding;

        9) 設定SofaModuleApplicationContext中BeanFactory的beanClassLoader為子產品的類加載器;

        10) 設定SofaModuleApplicationContext中BeanFactory的propertyEditorRegistrar為PropertyEditorRegistrar;

        11) 設定DeploymentDescriptor的應用上下文為SofaModuleApplicationContext;

        12) 建立并設定XmlBeanDefinitionReader執行個體beanDefinitionReader;

        13) 依次周遊SOFABoot子產品中META-INF/spring目錄下所有spring的XML配置檔案,并使用beanDefinitionReader加載XML檔案中配置的Bean。此時表明子產品的Spring應用上下文隻加載子產品META-INF/spring下Spring XML配置檔案中的Bean。此處需要注意的是,如果在子產品中采用自動配置的方式,即通過@Configuration、@Bean注解某個配置類,并在該子產品的META-INF/spring.factories檔案中配置org.springframework.boot.autoconfigure.EnableAutoConfiguration項,則這些Bean會被加載到根Spring應用上下文,而不是子產品對應的Spring應用上下文。

        14) 從根Spring應用上下文AnnotationConfigEmbeddedWebApplicationContext中擷取名為PROCESSORS_OF_ROOT_APPLICATION_CONTEXT的Bean,該Bean為一個Map,緩存了根Spring應用上下文中的所有Post Processors,本項目主要包括:

1.  org.springframework.boot.context.properties.ConfigurationBeanFactoryMetaData

2.  org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration

3.  org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration

4.  com.alipay.sofa.runtime.spring.ServiceAnnotationBeanPostProcessor

5.  com.alipay.sofa.isle.spring.listener.SofaModuleBeanFactoryPostProcessor

6.  com.alipay.sofa.runtime.spring.SofaRuntimeContextAwareProcessor

7.  com.alipay.sofa.runtime.spring.ClientFactoryBeanPostProcessor

8.  org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor

9.  org.springframework.boot.web.servlet.ErrorPageRegistrarBeanPostProcessor

10. org.springframework.context.annotation.ConfigurationClassPostProcessor

11. org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizerBeanPostProcessor

        至此,建立完一個SOFABoot子產品對應的Spring應用上下文,然後恢複目前線程的上下文類加載器。

        (四)    建立完所有SOFABoot子產品對應的Spring應用上下文以後,應該重新整理Spring應用上下文。

        SOFABoot支援兩種重新整理SOFABoot子產品Spring應用上下文方式:

        1.   串行重新整理:

        周遊所有子產品級别的Spring應用上下文,依次調用每個Spring應用上下文的refresh方法,完成重新整理操作。此模式隻适用于子產品之間沒有依賴的情況。該模式在子產品比較多的情況下,重新整理時間比較長,性能較低;

        2.   并行重新整理:

        利用Java線程池,采用多線程并行重新整理子產品的Spring應用上下文。此模式适用于所有子產品重新整理情況,即子產品之間存在依賴關系和子產品之間不存在依賴關系。此模式性能較高;

        根據SofaModuleProperties執行個體中moduleStartUpParallel屬性值,選擇重新整理SOFABoot子產品Spring應用上下文的方式。在application.properties檔案中,com.alipay.sofa.boot.module-start-up-parallel=true,是以此處采用并行重新整理方式。

1.  private voidrefreshSpringContextParallel(ApplicationRuntimeModel application) {

2.          ClassLoader oldClassLoader =Thread.currentThread().getContextClassLoader();

3.          List<DeploymentDescriptor>coreRoots = new ArrayList<>();

4.          ThreadPoolExecutor executor = newThreadPoolExecutor(CPU_COUNT + 1, CPU_COUNT + 1, 60,

5.              TimeUnit.MILLISECONDS, newSynchronousQueue<Runnable>(), new NamedThreadFactory(

6.                  "sofa-module-start"),new ThreadPoolExecutor.CallerRunsPolicy());

7.          try {

8.              for (DeploymentDescriptor deployment :application.getResolvedDeployments()) {

9.                  DependencyTree.Entry entry =application.getDeployRegistry().getEntry(

10.                     deployment.getModuleName());

11.                 if (entry != null &&entry.getDependencies() == null) {

12.                     coreRoots.add(deployment);

13.                 }

14.             }

15.             refreshSpringContextParallel(coreRoots,application.getResolvedDeployments().size(),

16.                 application, executor);

17.  

18.         } finally {

19.             executor.shutdown();

20.             Thread.currentThread().setContextClassLoader(oldClassLoader);

21.         }

22.     }

        建立執行線程池executor,此時需要注意以下幾個參數:

        1) 核心線程數與最大可用線程數相同:建立線程池時初始化核心線程,運作過程中不在建立線程,避免不必要的線程建立與回收,提高性能;

        2) 阻塞隊列為SynchronousQueue:當送出Spring應用上下文重新整理任務時,如果此時線程池中所有線程已經都被占用,則阻塞在送出任務處,等待其它重新整理任務完成,有空閑的線程接受新送出的任務;

        3) 拒絕執行政策為CallerRunsPolicy,即由調用者線程來執行被拒絕的任務:這樣當重新整理任務比較多時,可以利用調用線程本身執行重新整理任務,提高性能;

        依次周遊ApplicationRuntimeModel執行個體application中每個DeploymentDescriptor執行個體(每個執行個體代表一個子產品),判斷DeploymentDescriptor執行個體的dependencies屬性是否為空,為空表示此子產品不依賴其它子產品或所依賴的子產品已經重新整理完成,本子產品可以重新整理了,則把DeploymentDescriptor執行個體加入數組清單coreRoots,等待下一步重新整理操作。否則,表示此子產品所依賴的子產品還沒有重新整理,本子產品不可用重新整理。

        調用refreshSpringContextParallel方法,重新整理已具備重新整理條件的子產品的Spring應用上下文。

1.  private voidrefreshSpringContextParallel(List<DeploymentDescriptor> rootDeployments,

2.                                                inttotalSize,

3.                                                finalApplicationRuntimeModel application,

4.                                                finalThreadPoolExecutor executor) {

5.          ……略

6.   

7.          final CountDownLatch latch = newCountDownLatch(totalSize);

8.          List<Future> futures = newCopyOnWriteArrayList<>();

9.   

10.         for (final DeploymentDescriptor deployment :rootDeployments) {

11.             refreshSpringContextParallel(deployment,application, executor, latch, futures);

12.         }

13.  

14.         try {

15.             latch.await();

16.         } catch (InterruptedException e) {

17.             throw newRuntimeException("Wait for Sofa Module Refresh Fail", e);

18.         }

19.  

20.         for (Future future : futures) {

21.             try {

22.                 future.get();

23.             } catch (Throwable e) {

24.                 throw new RuntimeException(e);

25.             }

26.         }

27.  

28.     }

        refreshSpringContextParallel方法主要處理邏輯如下:

        1) 以所有已經解析完成的子產品數為初始值,建立CountDownLatch執行個體latch。在此主重新整理線程把所有重新整理任務送出給執行線程池executor以後,執行await操作,等待所有的重新整理線程完成操作。而每個重新整理Spring應用上下文的線程結束時,latch執行countDown操作,通知調用線程此重新整理線程執行完成。

        2) 送出重新整理任務到線程池executor。

        3) 依次周遊futures中代表重新整理任務的future,執行每個future的get方法,阻塞主重新整理線程,等待所有的重新整理任務的完成。

        其中,送出重新整理任務到線程池的代碼如下:

1.  private voidrefreshSpringContextParallel(final DeploymentDescriptor deployment,

2.                                                finalApplicationRuntimeModel application,

3.                                                finalThreadPoolExecutor executor,

4.                                                finalCountDownLatch latch, final List<Future> futures) {

5.          futures.add(executor.submit(new Runnable() {

6.              @Override

7.              public void run() {

8.                  String oldName =Thread.currentThread().getName();

9.                  try {

10.                     ……略

11.                     if(deployment.isSpringPowered()) {

12.                         doRefreshSpringContext(deployment,application);

13.                     }

14.                     DependencyTree.Entry<String,DeploymentDescriptor> entry = application

15.                         .getDeployRegistry().getEntry(deployment.getModuleName());

16.                     if (entry != null&& entry.getDependsOnMe() != null) {

17.                         for (final DependencyTree.Entry<String,DeploymentDescriptor> child : entry

18.                             .getDependsOnMe()) {

19.                             child.getDependencies().remove(entry);

20.                             if (child.getDependencies().size() == 0) {

21.                                 refreshSpringContextParallel(child.get(),application, executor,

22.                                     latch,futures);

23.                             }

24.                         }

25.                     }

26.                 } catch (Throwable e) {

27.                     ……略

28.                 } finally {

29.                     latch.countDown();

30.                     Thread.currentThread().setName(oldName);

31.                 }

32.             }

33.         }));

34.     }

        refreshSpringContextParallel方法主要處理邏輯如下:

        1) 建立匿名的重新整理任務,并送出到線程池,然後把代表該任務執行結果的future增加到futures清單,供後續查詢重新整理結果。

        2) 在重新整理任務中,首先判斷子產品是否為Spring子產品,如果是則調用doRefreshSpringContext方法執行Spring上下文重新整理操作。否則,不重新整理本子產品。

        3) 執行完本子產品的Spring應用上下文重新整理以後,擷取所有依賴本子產品的其它子產品。依次把其它子產品的依賴集合dependencies中去掉本子產品。如果去掉以後,dependencies的size為0,表示所依賴的子產品已經重新整理完成,自己可以被重新整理了,則調用refreshSpringContextParallel方法,送出子產品重新整理任務到線程池。

        4) 送出完其它子產品的重新整理任務,則調用countDown方法,通知主線程本子產品重新整理任務完成,把執行線程傳回線程池,供其它重新整理任務使用。

        依次循環,直至所有子產品的Spring應用上下文重新整理完成。針對每個Spring應用上下文,其重新整理工作在doRefreshSpringContext方法完成:

1.  private voiddoRefreshSpringContext(DeploymentDescriptor deployment,

2.                                          ApplicationRuntimeModelapplication) {

3.          ……略

4.          ConfigurableApplicationContext ctx =(ConfigurableApplicationContext) deployment.getApplicationContext();

5.          if (ctx != null) {

6.              try {

7.                  deployment.startDeploy();

8.                  ctx.refresh();

9.                  application.addInstalled(deployment);

10.             } catch (Throwable t) {

11.                ……略

12.             } finally {

13.                 deployment.deployFinish();

14.             }

15.         } else {

16.             application.addFailed(deployment);

17.             ……略       }

18.     }

        看到ctx.refresh()這段代碼,大家就應該比較熟悉了,就是具體的執行Spring應用上下文重新整理操作,這個大家都比較熟悉了,在此就不詳述了。

        三、  執行ModuleLogOutputStage的process方法:

        所有SOFABoot子產品的Spring應用上下文重新整理完成以後,執行ModuleLogOutputStage的process方法,記錄子產品加載過程中相關的日志。

1.      protected void doProcess() throws Exception{

2.          ApplicationRuntimeModel application =applicationContext.getBean(

3.              SofaModuleFrameworkConstants.APPLICATION,ApplicationRuntimeModel.class);

4.   

5.          StringBuilder stringBuilder = newStringBuilder();

6.          logInstalledModules(stringBuilder,application.getInstalled());

7.          logFailedModules(stringBuilder,application.getFailed());

8.          logInfoBeanCost(stringBuilder,application.getInstalled());

9.   

10.         SofaLogger.info(stringBuilder.toString());

11.     }

        此處主要記錄如下内容:

        1) 加載成功的子產品;

        2) 加載失敗的子產品;

        3) 加載成功的子產品中Bean加載時間超過指定加載時限(此處:100MS)的Bean。

        到此為止,SOFABoot應用啟動完成。

繼續閱讀