1. 程式人生 > >java複習筆記2--SpringCloud系列一:微服務啟動原理探索

java複習筆記2--SpringCloud系列一:微服務啟動原理探索

微服務架構的趨勢

隨著資料量的不斷增大,大資料時代的到來,網際網路技術的不斷髮展和變革,微服務架構和雲服務平臺以及大資料成為了時下最熱門的話題。現在,比較流行的微服務框架也有很多, 比如阿里的Dubbo,基於soringBoot的SpringCloud,Apache的thrift,google的Grpc,這些都提供了很多高併發以及負載均衡等的一系列的解決方案。而我們今天的主角,就是熱度最高的SpringCloud,目前只支援java。

什麼是SpringCloud

springCloud是基於SpringBoot的一整套實現微服務的框架。他提供了微服務開發所需的配置管理、服務發現、斷路器、智慧路由、微代理、控制匯流排、全域性鎖、決策競選、分散式會話和叢集狀態管理等元件。

而SpringCloud之所以又這麼完善的解決方案和體系,得益於他相容了很多優秀的開源的框架,這個之後會具體的一個個溫習。今天,我們就先從SpringCloud的啟動開始看,學習一下他的啟動和載入過程。

SpringCloud的啟動方式

相比大家對於SpringBoot或者SpringCloud都不陌生,他的啟動方式很簡單:

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class EurekaClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(EurekaClientApplication.class, args);
	}
}

當然,也可以分開寫,像下面這樣:

@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class EurekaClientApplication {

	public static void main(String[] args) {
		SpringApplication application = new SpringApplication(EurekaClientApplication.class);
		application.run(args);
	}
}

其實二者的本質是完全一樣的。@SpringBootApplication註解就是備註這個服務為SpringBoot服務,關於註解這裡就先不詳細講了,之後會單獨花時間去詳細說明,結合反射一起。今天,先重點弄清楚啟動流程以及都做了什麼。我們就按照第二種模式,分兩個方法進行探索。

初始化SpringApplication

首先看第一行程式碼,SpringApplication application = new SpringApplication(EurekaClientApplication.class);這個是一個簡單的構造方法,大概能夠猜測是例項化一個SpringApplication。接下來上程式碼:

    public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = this.deduceWebApplicationType();
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

重點來了,這裡主要做了三件事。

  • 1.屬性賦值,這個很好理解
  • 2.檢查服務的型別,web專案還是普通專案,也就是this.webApplicationType = this.deduceWebApplicationType();這行程式碼。推斷當前環境是哪種Web環境(Servlet、Reactive),或者不是Web環境,判斷邏輯是Classpath是否有相應環境需要的類,比如DispatcherHandler,.servlet.DispatcherServlet,程式碼思路比較清晰,原始碼就不貼了,大家有時間可以去看一下。
  • 3.初始化initializers,它是一個用來存放SpringApplication所需的ApplicationContextInitializer例項的ArrayList,這些例項是用來初始化應用程式的上下文環境的。走讀程式碼發現,這裡會載入所有繼承 ApplicationContextInitializer的實現類。在Spring中也有一種類似玉Java SPI(service provider interface 不瞭解的可以百度瞭解一下)的載入機制。它在META-INF/spring.factories檔案中配置介面的實現類名稱,然後在程式中讀取這些配置檔案並例項化。這種自定義的SPI機制是Spring Boot Starter實現的基礎。
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);

這兩行程式碼就是具體實現,他會載入所有spring包下的META-INF/spring.factories,裡面會有類實現介面的描述,這樣,通過介面,可以找到所有的實現類,進而初始化這些類的例項物件。 在這裡插入圖片描述 上面有具體的路徑,大家可以點進去看一下,具體的介面和實現類的描述和排版,類似於一個鍵值對的關係。

  • 4.初始化listeners,方法和上面雷同,匯入所有的ApplicationListener的實現類並建立例項物件。
  • 5.deduceMainApplicationClass()推斷當前的應用程式的入口類,即我們的EurekaClientApplication.class。因為這裡入參可以是多個,維護的是一個set,所以會執行這個操作,對於我們這個來說,mainApplicationClass = EurekaClientApplication.class。

啟動 Spring 應用程式

這裡,就是我們之前啟動SpringCloud的第二行程式碼,呼叫SpringApplicaiton的run方法。這裡程式碼模組較複雜,建議debug模式下一步步細看,首先,我們貼出原始碼:

public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, new Object[]{context});
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if(this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            listeners.started(context);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

  • 首先,啟動監聽StopWatch,主要進行響應時間的監聽和輸出
  • 設定java.awt.headless模式,來進行簡單的影象處理,無需呼叫server裝置,很好解決linux環境下可能沒有圖形處理或者裝置工具而帶來的報錯。這也是前面初始化SpringApplication的時候將java.awt.headless設定為true的原因。
  • 載入SpringApplicationRunListeners實現類,還是熟悉的配方,遍歷spring.factory,以介面作為key找尋對應的value。並且觸發開始的事件。
  • 接下來幾個就是重頭戲了。。首先,準備執行環境,prepareEnvironment,這裡會根據之前設定的WebApplicationType和webenvironment來建立對應的執行環境,這裡建立的是StandardServletEnvironment。具體程式碼就不貼出來了,它主要有兩個方法,一個是設定一些PropertySources引數的方法,第二個方法是在後面的SpringApplication的refresh方法中呼叫的。
  • configureEnvironment(environment, applicationArguments.getSourceArgs())這個方法不涉及到任何操作,大家可以瀏覽一下就跳過。
  • printBanner就是我們經常說的列印的那個spring的日誌,大家可以寫一個檔案替換它。
  • createApplicationContext方法,是根據SpringApplication的webApplicationType來例項化對應的上下文,而webApplicationType就是第一大步裡面判斷並初始化的;如果webApplicationType的值是SERVLET,那麼例項化AnnotationConfigServletWebServerApplicationContext,如果是REACTIVE則例項化AnnotationConfigReactiveWebServerApplicationContext(響應式程式設計,後續再看),如果既不是SERVLET、也不是REACTIVE,那麼則是預設情況(也就是我們所說的非web引用),例項化AnnotationConfigApplicationContext。web應用這裡就是例項化了AnnotationConfigServletWebServerApplicationContext。
  • prepareContext,這個方法,看名字就能想到是準備上下文,也就是往我們上一步生成的AnnotationConfigServletWebServerApplicationContext上下文中繼續填充屬性。這裡方法較多,主要是將context中的environment替換成SpringApplication中建立的environment,將SpringApplication中的initializers應用到context中,將SpringApplication中的listeners註冊到context中,載入兩個單例bean到beanFactory中,初始化資源載入器BeanDefinitionLoader,並廣播ApplicationPreparedEvent事件。
  • refreshContext方法做的事情更多,這裡,才是建立了一個真正的Spring上下文容器。由於這裡東西較多,涉及程式碼量較大,明天再從頭到尾跟一遍。不漏一個細節,徹底弄清楚載入的流程。之後也會深入挖掘啟動tomcat等的原理和流程。希望有興趣的可以一起探討。