SpringBoot啓動源碼分析及相關技巧學習

對於源碼學習,我以爲咱們帶着問題一塊兒看會好一點。css

1、Springboot的啓動原理是怎樣的?

話很少說,咱們首先去[start.spring.io]網站上下載一個demo,springboot版本咱們選擇2.1.4,而後咱們一塊兒打斷點一步步瞭解下springboot的啓動原理。java

咱們的工程目錄以下:web

一切的一切,將從咱們的DemoApplication.java文件開始。代碼以下:spring

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

技巧一緩存

我常常看到有朋友在DemoApplication類中實現ApplicationContextAware接口,而後獲取ApplicationContext對象,就好比下面的代碼:tomcat

@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上下文返回了,咱們直接用就好了~~~代碼以下:springboot

@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類第263bash

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

步驟1-3沒什麼好講的,就是初始化一些標識和列表啥的,重點看下第4和第5點,第四、5點幫咱們加載了全部依賴的ApplicationListenerApplicationContextInitializer配置項,代碼移步至SpringFactoriesLoader132行,咱們能夠看到springboot會去加載每一個jar裏邊這個文件META-INF/spring.factories的內容,同時還以類加載器ClassLoader爲鍵值,對全部的配置作了一個Map緩存。app

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看下內容:less

# 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) {
    // 一、計時器,spring內部封裝的計時器,用於計算容器啓動的時間
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 二、建立一個初始化上下文變量
    ConfigurableApplicationContext context = null;
    // 三、這是spring報告之類的,沒深刻了解
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    configureHeadlessProperty();
    // 四、獲取配置的SpringApplicationRunListener類型的監聽器,而且啓動它
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
    	ApplicationArguments applicationArguments = new DefaultApplicationArguments(
    			args);
    	// 五、準備spring上下文環境
    	ConfigurableEnvironment environment = prepareEnvironment(listeners,
    			applicationArguments);
    	configureIgnoreBeanInfo(environment);
    	// 六、打印banner
    	Banner printedBanner = printBanner(environment);
    	// 七、爲context賦值
    	context = createApplicationContext();
    	exceptionReporters = getSpringFactoriesInstances(
    			SpringBootExceptionReporter.class,
    			new Class[] { ConfigurableApplicationContext.class }, context);
    	// 八、準備好context上下文各類組件,environment,listeners
    	prepareContext(context, environment, listeners, applicationArguments,
    			printedBanner);
    	// 九、刷新上下文
    	refreshContext(context);
    	afterRefresh(context, applicationArguments);
    	// 十、計時器關閉
    	stopWatch.stop();
    	if (this.logStartupInfo) {
    		new StartupInfoLogger(this.mainApplicationClass)
    				.logStarted(getApplicationLog(), stopWatch);
    	}
    	listeners.started(context);
    	// 十一、調用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, ex, exceptionReporters, 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");
	}

}
複製代碼

注意點:

  • 一、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機制完成的。

3、總結

本文從大體方向解析了springboot的大體啓動過程,有些地方點到爲止,並未作深刻研究,但咱們學習源碼一爲了吸取其編碼精華,寫出更好的代碼,二爲了解相關原理,方便更加快速定位解決問題,若有寫的不對的地方,請指正,歡迎評論區留言交流,謝謝你們!

相關文章
相關標籤/搜索