1. 程式人生 > 程式設計 >SpringBoot啟動原始碼分析及相關技巧學習

SpringBoot啟動原始碼分析及相關技巧學習

對於原始碼學習,我覺得我們帶著問題一起看會好一點。

一、Springboot的啟動原理是怎樣的?

話不多說,我們首先去[start.spring.io]網站上下載一個demo,springboot版本我們選擇2.1.4,然後我們一起打斷點一步步瞭解下springboot的啟動原理。

我們的工程目錄如下:

一切的一切,將從我們的DemoApplication.java檔案開始。程式碼如下:

@SpringBootApplication
public class DemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class,args);
	}
}
複製程式碼

技巧一

我經常看到有朋友在DemoApplication類中實現ApplicationContextAware介面,然後獲取ApplicationContext物件,就比如下面的程式碼:

@SpringBootApplication
public class DemoApplication implements ApplicationContextAware {
    private static ApplicationContext applicationContext = null;
    public static void main(String[] args) {
    	SpringApplication.run(DemoApplication.class,args);
    	// 獲取某個bean
    	System.out.println(applicationContext.getBean("xxxx"
)); } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { DemoApplication.applicationContext = applicationContext; } } 複製程式碼

當然這種方法可行,但是其實SpringApplication.run方法已經把Spring上下文返回了,我們直接用就行了~~~程式碼如下:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
    	ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class,args);
    	// 獲取某個bean
    	System.out.println(applicationContext.getBean("xxxx"
)); } } 複製程式碼

程式碼跳至SpringApplication類第263

@SuppressWarnings({ "unchecked","rawtypes" })
    public SpringApplication(ResourceLoader resourceLoader,Class<?>... primarySources) {
        // 1、初始化一個類載入器
    	this.resourceLoader = resourceLoader;
    	Assert.notNull(primarySources,"PrimarySources must not be null");
    	// 2、啟動類集合
    	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    	// 3、當前應用型別,有三種:NONE、SERVLET、REACTIVE
    	this.webApplicationType = WebApplicationType.deduceFromClasspath();
    	// 4、初始化Initializer
    	setInitializers((Collection) getSpringFactoriesInstances(
    			ApplicationContextInitializer.class));
    	// 5、初始化Listeners
    	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    	// 6、初始化入口類
    	this.mainApplicationClass = deduceMainApplicationClass();
    }
複製程式碼

步驟1-3沒什麼好講的,就是初始化一些標識和列表啥的,重點看下第4和第5點,第4、5點幫我們載入了所有依賴的ApplicationListenerApplicationContextInitializer配置項,程式碼移步至SpringFactoriesLoader132行,我們可以看到springboot會去載入每個jar裡邊這個檔案META-INF/spring.factories的內容,同時還以類載入器ClassLoader為鍵值,對所有的配置做了一個Map快取。

private static Map<String,List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // cache做了快取,我們可以指定classloader,預設為Thread.currentThread().getContextClassLoader();
    // (可在ClassUtils類中getDefaultClassLoader找到答案)
    MultiValueMap<String,String> result = cache.get(classLoader);
    if (result != null) {
    	return result;
    }
    
    try {
    	Enumeration<URL> urls = (classLoader != null ?
    	        // FACTORIES_RESOURCE_LOCATION的值就是META-INF/spring.factories
    			classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
    			ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    	result = new LinkedMultiValueMap<>();
    	while (urls.hasMoreElements()) {
    		URL url = urls.nextElement();
    		UrlResource resource = new UrlResource(url);
    		Properties properties = PropertiesLoaderUtils.loadProperties(resource);
    		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);
    }
}
複製程式碼

我們簡單看下spring-boot-autoconfigure-2.1.4.RELEASE.jar下的spring.factories看下內容:

# Initializers初始化器
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners監聽器
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configure自動配置(下文將會有講原理)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
......
複製程式碼

技巧二

接下來我們看下步驟6,這裡可以學習一個小技巧,我們如何獲得當前方法呼叫鏈中某一箇中間方法所在的類資訊呢?我們看原始碼:

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;
}
複製程式碼

到目前為止,我們只完成了SpringApplication這個類的初始化工作,我們擁有了META-INF/spring.factories目錄下配置的包括監聽器、初始化器在內的所有類名,並且例項化了這些類,最後儲存於SpringApplication這個類中。

程式碼移步至SpringApplication.java295行,程式碼如下

public ConfigurableApplicationContext run(String... args) {
    // 1、計時器,spring內部封裝的計時器,用於計算容器啟動的時間
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 2、建立一個初始化上下文變數
    ConfigurableApplicationContext context = null;
    // 3、這是spring報告之類的,沒深入瞭解
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    // 4、獲取配置的SpringApplicationRunListener型別的監聽器,並且啟動它
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
    	ApplicationArguments applicationArguments = new DefaultApplicationArguments(
    			args);
    	// 5、準備spring上下文環境
    	ConfigurableEnvironment environment = prepareEnvironment(listeners,applicationArguments);
    	configureIgnoreBeanInfo(environment);
    	// 6、列印banner
    	Banner printedBanner = printBanner(environment);
    	// 7、為context賦值
    	context = createApplicationContext();
    	exceptionReporters = getSpringFactoriesInstances(
    			SpringBootExceptionReporter.class,new Class[] { ConfigurableApplicationContext.class },context);
    	// 8、準備好context上下文各種元件,environment,listeners
    	prepareContext(context,environment,listeners,applicationArguments,printedBanner);
    	// 9、重新整理上下文
    	refreshContext(context);
    	afterRefresh(context,applicationArguments);
    	// 10、計時器關閉
    	stopWatch.stop();
    	if (this.logStartupInfo) {
    		new StartupInfoLogger(this.mainApplicationClass)
    				.logStarted(getApplicationLog(),stopWatch);
    	}
    	listeners.started(context);
    	// 11、呼叫runners,後面會講到
    	callRunners(context,applicationArguments);
    }
    catch (Throwable ex) {
    	handleRunFailure(context,ex,exceptionReporters,listeners);
    	throw new IllegalStateException(ex);
    }
    
    try {
    	listeners.running(context);
    }
    catch (Throwable ex) {
    	handleRunFailure(context,null);
    	throw new IllegalStateException(ex);
    }
    return context;
}
複製程式碼

技巧三

步驟1中使用到了計時器StopWatch這個工具,這個工具我們也可以直接拿來使用的,通常我們統計一段程式碼、一個方法執行的時間,我們會使用System.currentTimeMillis來實現,我們也可以使用StopWatch來代替,StopWatch的強大之處在於它可以統計各個時間段的耗時佔比,使用大致如下:

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
    	StopWatch stopWatch = new StopWatch();
    	stopWatch.start("startContext");
    	ConfigurableApplicationContext applicationContext = SpringApplication.run(DemoApplication.class,args);
    	stopWatch.stop();
    	stopWatch.start("printBean");
    	// 獲取某個bean
    	System.out.println(applicationContext.getBean(DemoApplication.class));
    	stopWatch.stop();
    	System.err.println(stopWatch.prettyPrint());
    }
}
複製程式碼

步驟4程式碼移步至SpringApplication413行,程式碼如下:

private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[] { SpringApplication.class,String[].class };
    return new SpringApplicationRunListeners(logger,getSpringFactoriesInstances(
		SpringApplicationRunListener.class,types,this,args));
}
複製程式碼

可以看出,springboot依舊是去META-INF/spring.factoriesSpringApplicationRunListener配置的類,並且啟動。

步驟5預設建立Spring Environment模組中的StandardServletEnvironment標準環境。

步驟7預設建立的上下文型別是AnnotationConfigServletWebServerApplicationContext,可以看出這個是Spring上下文中基於註解的Servlet上下文,因此,我們最開始的DemoApplication.java類中宣告的註解@SpringBootApplication將會被掃描並解析。

步驟9重新整理上下文是最核心的,看過spring原始碼都知道,這個refresh()方法很經典,具體可以參考小編另一篇文章Spring容器IOC初始化過程

步驟11中會執行整個上下文中,所有實現了ApplicationRunnerCommandLineRunner的bean,SpringApplication787程式碼如下:

private void callRunners(ApplicationContext context,ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
    // 對所有runners進行排序並執行
    AnnotationAwareOrderComparator.sort(runners);
    for (Object runner : new LinkedHashSet<>(runners)) {
    	if (runner instanceof ApplicationRunner) {
    		callRunner((ApplicationRunner) runner,args);
    	}
    	if (runner instanceof CommandLineRunner) {
    		callRunner((CommandLineRunner) runner,args);
    	}
    }
}
複製程式碼

技巧四】 平時開發中,我們可能會想在Spring容器啟動完成之後執行一些操作,舉個例子,就假如我們某個定時任務需要再應用啟動完成時執行一次,看了上面步驟11的原始碼,我們大概對下面的程式碼會恍然大悟,哦,原來這程式碼就是在SpringApplication這個類中呼叫的。

@Component
public class MyRunner implements ApplicationRunner {

	@Override
	public void run(ApplicationArguments args) throws Exception {
		System.err.println("執行了ApplicationRunner~");
	}

}
複製程式碼
@Component
public class MyCommandRunner implements CommandLineRunner {

	@Override
	public void run(String... args) throws Exception {
		System.out.println("執行了commandrunner");
	}

}
複製程式碼

注意點:

  • 1、CommandLineRunner和ApplicationRunner執行時期是在spring容器啟動完成之後執行的
  • 2、整個容器生命週期只執行一次

二、註解@EnableAutoConfiguration的作用是什麼?

一般情況下,java引入的jar檔案中宣告的bean是不會被spring掃描到的,那麼我們的各種starter是如何初始化自身的bean呢?答案是在META-INF/spring.factories中宣告org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx,就比如spring-cloud-netflix-zuul這個starter中申明的內容如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.zuul.ZuulServerAutoConfiguration,\
org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration
複製程式碼

這樣宣告是什麼意思呢?就是說springboot啟動的過程中,會將org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx宣告的類例項成為bean,並且註冊到容器當中,下面是測試用例:

我們在mydemo中宣告一個bean,程式碼如下:

@Service
public class MyUser {

}
複製程式碼

demo中,列印MyUser這個bean,列印如下:

Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'css.demo.user.MyUser' available
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:343)
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:335)
	at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
	at com.example.demo.DemoApplication.main(DemoApplication.java:15)
複製程式碼

我們mydemo工程中加上該配置:

demo工程列印如下:

2019-08-02 19:31:34.814  INFO 21984 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2019-08-02 19:31:34.818  INFO 21984 --- [           main] com.example.demo.DemoApplication         : Started DemoApplication in 2.734 seconds (JVM running for 3.254)
執行了ApplicationRunner~
執行了commandrunner
css.demo.user.MyUser@589b028e
複製程式碼

為什麼配置上去就可以了呢?其實在springboot啟動過程中,在AutoConfigurationImportSelector#getAutoConfigurationEntry中會去呼叫getCandidateConfigurations方法,該方法原始碼如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,AnnotationAttributes attributes) {
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
            // 此處會去呼叫EnableAutoConfiguration註解
    		getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());
    Assert.notEmpty(configurations,"No auto configuration classes found in META-INF/spring.factories. If you "
    				+ "are using a custom packaging,make sure that file is correct.");
    return configurations;
}
複製程式碼

getSpringFactoriesLoaderFactoryClass方法原始碼如下:

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
	return EnableAutoConfiguration.class;
}
複製程式碼

本質上還是利用了META-INF/spring.factories檔案中的配置,結合springboot factories機制完成的。

三、總結

本文從大致方向解析了springboot的大致啟動過程,有些地方點到為止,並未做深入研究,但我們學習原始碼一為了吸收其編碼精華,寫出更好的程式碼,二為瞭解相關原理,方便更加快速定位解決問題,如有寫的不對的地方,請指正,歡迎評論區留言交流,謝謝大家!