Springboot啟動(dòng)流程詳細(xì)分析
springboot啟動(dòng)是通過(guò)一個(gè)main方法啟動(dòng)的,代碼如下
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
從該方法我們一路跟進(jìn)去,進(jìn)入SpringApplication的構(gòu)造函數(shù),我們可以看到如下代碼primarySources,為我們從run方法塞進(jìn)來(lái)的主類,而resourceLoader此處為null,是通過(guò)構(gòu)造函數(shù)重載進(jìn)來(lái),意味著這里還有其它方式的用法,先繞過(guò)。
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); //此處推斷web容器類型是servlet 還是REACTIVE還是啥也不是即當(dāng)前非web容器 this.webApplicationType = WebApplicationType.deduceFromClasspath(); //此處進(jìn)入springFacories的加載 即SpringFactoriesLoader,不過(guò)此處是過(guò)濾處所有ApplicationContextInitializer的子類 setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); //從springFactories的緩存中再過(guò)濾處 ApplicationListener 的子類 setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }
所以我們來(lái)看SpringFactoriesLoader中的loadSpringFactories方法,先獲取資源路徑META-INF/spring.factories
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = cache.get(classLoader); if (result != null) { return result; } try { //classLoader 上一步傳進(jìn)來(lái)的為AppClassLoader,如果對(duì)classLoader不了解需要去看看java的類加載機(jī)制,以及雙親委托機(jī)制,此處的資源加載也是個(gè)雙親委托機(jī)制。 //此處如果appclassLoader不為null,那么遍歷遍歷這個(gè)classLoader所加載的包,中是否存在META-INF/spring.factories這個(gè)文件 //如果存在最終生成一個(gè)集合,然后遍歷集合 Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); //從集合中取出一個(gè)spring.factories文件 UrlResource resource = new UrlResource(url); //讀取對(duì)應(yīng)文件內(nèi)容,作為一個(gè)properties對(duì)象 Properties properties = PropertiesLoaderUtils.loadProperties(resource); //解析文件內(nèi)容,并將之放入一個(gè)LinkedMultiValueMap中,key就是spring.factories中等號(hào)前面的部分,值是后面部分根據(jù)都好組成的list。并放入緩存?zhèn)溆?,緩存的key為對(duì)應(yīng)的classLoader for (Map.Entry<?, ?> entry : properties.entrySet()) { String factoryClassName = ((String) entry.getKey()).trim(); for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryClassName, factoryName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }
以上代碼執(zhí)行完成后形成的內(nèi)容大概是這樣的,key表示factoryClassName
result = new LinkedMultiValueMap<>(); list=new LinkList(); list.add("com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration"); list.add("org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration") result.put("org.springframework.boot.autoconfigure.EnableAutoConfiguration",list);
所以此時(shí)回退到這個(gè)方法setInitializers((Collection)
getSpringFactoriesInstances(ApplicationContextInitializer.class));
就是從遍歷出來(lái)的所有的這些內(nèi)容中獲取出key為org.springframework.context.ApplicationContextInitializer 的集合
設(shè)置該當(dāng)前類的initializers集合,后面的Listeners 集合也是一樣的過(guò)程。只不過(guò),后一次獲取是從上一次的緩存中取的。
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList()); }
設(shè)置完initializer和listener集合后,進(jìn)行主函數(shù)推斷
接著看代碼,此處對(duì)主函數(shù)的推斷非常的巧妙,就是利用虛擬機(jī)運(yùn)行時(shí)已被入棧的所有鏈路一路追蹤到方法名為main的函數(shù),然后獲取到他的類名。
private Class<?> deduceMainApplicationClass() { try { StackTraceElement[] stackTrace = new RuntimeException().getStackTrace(); for (StackTraceElement stackTraceElement : stackTrace) { if ("main".equals(stackTraceElement.getMethodName())) { return Class.forName(stackTraceElement.getClassName()); } } } catch (ClassNotFoundException ex) { // Swallow and continue } return null; }
此時(shí)已經(jīng)將SpringApplication 對(duì)象new出來(lái)了,然后接著執(zhí)行它的run方法,先總結(jié)下以上代碼干了幾個(gè)事情
- this.primarySources 設(shè)置了主類集合
- this.webApplicationType 設(shè)置了web容器類型
- setInitializers 設(shè)置了ApplicationContextInitializer
- setListeners 設(shè)置了ApplicationListener
- mainApplicationClass 設(shè)置了入口類
- 加載并且解析了所有的spring.factories文件并緩存起來(lái),注意此時(shí)只是解析成屬性。
我們發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象既然已經(jīng)有了this.primarySources 為什么還要來(lái)個(gè)mainApplicationClass呢?mainApplicationClass目前看來(lái)除了日志打印以及banner之外沒(méi)有什么其它作用。
接著看run方法
public ConfigurableApplicationContext run(String... args) { //計(jì)時(shí)器,不管它 StopWatch stopWatch = new StopWatch(); stopWatch.start(); ConfigurableApplicationContext context = null; Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); //告訴程序當(dāng)前為無(wú)頭模式,沒(méi)有外設(shè),如果需要調(diào)用外設(shè)接口需要你自己模擬,這些內(nèi)容再awt包中做了,所以你只要告訴它有沒(méi)有外設(shè)就可以了 configureHeadlessProperty(); //繼續(xù)從spring.factories中獲取對(duì)應(yīng)的對(duì)象,此時(shí)listeners中有一個(gè)默認(rèn)實(shí)現(xiàn)EventPublishingRunListener,如有需要這里是可進(jìn)行擴(kuò)展 SpringApplicationRunListeners listeners = getRunListeners(args); //遍歷所有的listeners并調(diào)用它的starting方法,廣播啟動(dòng)過(guò)程中的事件,注意這里是個(gè)廣播器不是監(jiān)聽(tīng)器,名字有點(diǎn)不好理解,實(shí)現(xiàn)了 //ApplicationListener的接口就接收到對(duì)應(yīng)事件之后執(zhí)行對(duì)應(yīng)操作,而我們前面也有設(shè)置了個(gè)listeners集合,為ApplicationListener //此處名字起的讓人容易誤解,明明是個(gè)廣播器非要叫RunListener listeners.starting(); try { //進(jìn)行控制臺(tái)參數(shù)解析,具體如何解析,此處不展開(kāi),后續(xù)對(duì)配置文件解析的文章在詳細(xì)介紹 ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); //此處配置個(gè)內(nèi)省beanInfo時(shí)是否緩存的開(kāi)關(guān),true就緩存,false,就不緩存 configureIgnoreBeanInfo(environment); //打印banner,這個(gè)不介紹了比較簡(jiǎn)單的東西 Banner printedBanner = printBanner(environment); //此處開(kāi)始創(chuàng)建容器,根據(jù)給定的容器類型也就是上面獲取到的webApplicationType創(chuàng)建對(duì)應(yīng)的容器 //servlet :org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext //reactive:org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext //default: org.springframework.context.annotation.AnnotationConfigApplicationContext 非web容器 context = createApplicationContext(); //異常解析器,如果出現(xiàn)異常了,會(huì)在被catch起來(lái),然后通過(guò)解析器鏈進(jìn)行解析。這個(gè)也是從spring.factories中獲取的 exceptionReporters = getSpringFactoriesInstances( SpringBootExceptionReporter.class, new Class[] { ConfigurableApplicationContext.class }, context); //上面創(chuàng)建了容器,這邊對(duì)容器做一些初始化操作 //1、設(shè)置環(huán)境變量env //2、執(zhí)行postProcessApplicationContext 準(zhǔn)備beanNameGenerator,resourceLoader,ApplicationConversionService //3、調(diào)用前面獲取到的ApplicationContextInitializer //4、廣播容器準(zhǔn)備完成事件 //5、添加了一個(gè)制定的單例就是banner打印用的 //6、在加載前,設(shè)置是否允許bean被覆蓋屬性,默認(rèn)false //7、開(kāi)始加載,入口為main函數(shù)傳入的primarySources,先創(chuàng)建個(gè)BeanDefinitionLoader,并將第二步準(zhǔn)備的實(shí)例設(shè)置給他, //最后由BeanDefinitionLoader.load()承當(dāng)所有的加載動(dòng)作,加載過(guò)程也很長(zhǎng),先按下不表。 //8、廣播容器加載事件 //到此容器準(zhǔn)備完成 prepareContext(context, environment, listeners, applicationArguments, printedBanner); //開(kāi)始執(zhí)行spring的refresh方法,此處不多介紹了 refreshContext(context); //容器refresh完成后留了個(gè)擴(kuò)展,也不知道是不是擴(kuò)展,這個(gè)方法需要去自定義啟動(dòng)類,有點(diǎn)奇怪 afterRefresh(context, applicationArguments); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } //然后廣播啟動(dòng)完成事件 listeners.started(context); //最后執(zhí)行所有的runners,也就是實(shí)現(xiàn)了ApplicationRunner接口的類是最后執(zhí)行,所以此時(shí)你可以做一些其它的動(dòng)作,比如注冊(cè)到注冊(cè)中心,比如啟動(dòng)netty等等。 callRunners(context, applicationArguments); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, listeners); throw new IllegalStateException(ex); } //在廣播一個(gè)運(yùn)行中的事件 try { listeners.running(context); } catch (Throwable ex) { handleRunFailure(context, ex, exceptionReporters, null); throw new IllegalStateException(ex); } return context; }
上述代碼中在prepareContext階段完成了第一階段的beanDefinition注冊(cè),此處僅注冊(cè)了第一bean 通過(guò)主類傳進(jìn)去的那個(gè)類。之后再refresh階段,通過(guò)解析該類進(jìn)行第二輪的beandefinition注冊(cè)。這一部分屬于spring-context的內(nèi)容了。
在refresh階段,因?yàn)橹黝愖鳛橐粋€(gè)配置類,自然也是需要進(jìn)行一輪解析的,所以接下來(lái)的步驟就是解析@SpringbootApplication,此注解為一個(gè)復(fù)合注解
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
內(nèi)容解析:
- @SpringBootConfiguration 等同于@Configuration
- @EnableAutoConfiguration
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import(AutoConfigurationImportSelector.class) public @interface EnableAutoConfiguration {
此處導(dǎo)入了一個(gè)AutoConfigurationImportSelector ,同時(shí)還通過(guò)@AutoConfigurationPackage導(dǎo)入了一個(gè)AutoConfigurationPackages.Registrar.class
- 接下來(lái)是一個(gè)@ComponentScan注解
而我們知道在spring的配置類(單一類)的解析過(guò)程中的順序是
- 先解析Component系列的注解
- 再解析@PropertySource
- 接著解析@ComponentScan與@ComponentScans注解
- 接著解析@Import
- 接著解析ImportResource
- 最后解析@Bean注解
而再Import中又分為幾種類型
ImportSelector
DeferredImportSelector
ImportBeanDefinitionRegistrar
普通的import類
他們的加載順序?yàn)?,普通的?ndash;>importSelector–deferredImportSelector–>ImportResource–>ImportBeanDefinitionRegistrar
綜上所述spring.factories聲明的配置類看來(lái)也不是墊底解析的,所以如果有遇到需要順序的場(chǎng)景可以參照這個(gè)順序來(lái)聲明即可。
到此這篇關(guān)于Springboot啟動(dòng)流程詳細(xì)分析的文章就介紹到這了,更多相關(guān)Springboot啟動(dòng)流程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java后臺(tái)實(shí)現(xiàn)js關(guān)閉本頁(yè)面,父頁(yè)面指定跳轉(zhuǎn)或刷新操作
這篇文章主要介紹了java后臺(tái)實(shí)現(xiàn)js關(guān)閉本頁(yè)面,父頁(yè)面指定跳轉(zhuǎn)或刷新操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11java多線程編程之使用runnable接口創(chuàng)建線程
實(shí)現(xiàn)Runnable接口的類必須使用Thread類的實(shí)例才能創(chuàng)建線程,通過(guò)Runnable接口創(chuàng)建線程分為以下兩步2014-01-01java日期格式化SimpleDateFormat的使用詳解
這篇文章主要介紹了java SimpleDateFormat使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05Java中try-catch的使用及注意細(xì)節(jié)
現(xiàn)在有很多的語(yǔ)言都支持try-catch,比如常見(jiàn)的就是c++,java等,這篇文章主要給大家介紹了關(guān)于Java中try-catch的使用及注意細(xì)節(jié)的相關(guān)資料,文中通過(guò)圖文以及實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-06-06Java反轉(zhuǎn)數(shù)組輸出實(shí)例代碼
這篇文章主要給大家介紹了關(guān)于Java反轉(zhuǎn)數(shù)組輸出以及利用Java實(shí)現(xiàn)字符串逆序輸出的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01Java實(shí)現(xiàn)經(jīng)典游戲2048的示例代碼
2014年Gabriele Cirulli利用周末的時(shí)間寫(xiě)2048這個(gè)游戲的程序。本文將用java語(yǔ)言實(shí)現(xiàn)這一經(jīng)典游戲,并采用了swing技術(shù)進(jìn)行了界面化處理,需要的可以參考一下2022-02-02Java異常繼承結(jié)構(gòu)解析_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
這篇文章主要介紹了Java異常繼承結(jié)構(gòu)解析的相關(guān)知識(shí),需要的朋友可以參考下2017-04-04MyEclipse打開(kāi)文件跳轉(zhuǎn)到notepad打開(kāi)問(wèn)題及解決方案
windows系統(tǒng)打開(kāi)README.md文件,每次都需要右鍵選擇notepad打開(kāi),感覺(jué)很麻煩,然后就把README.md文件打開(kāi)方式默認(rèn)選擇了notepad,這樣每次雙擊就能打開(kāi),感覺(jué)很方便,這篇文章主要介紹了MyEclipse打開(kāi)文件跳轉(zhuǎn)到notepad打開(kāi)問(wèn)題,需要的朋友可以參考下2024-03-03