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點幫我們載入了所有依賴的ApplicationListener
和ApplicationContextInitializer
配置項,程式碼移步至SpringFactoriesLoader
第132
行,我們可以看到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.java
第295
行,程式碼如下
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程式碼移步至SpringApplication
第413
行,程式碼如下:
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.factories
找SpringApplicationRunListener
配置的類,並且啟動。
步驟5預設建立Spring Environment模組中的StandardServletEnvironment
標準環境。
步驟7預設建立的上下文型別是AnnotationConfigServletWebServerApplicationContext
,可以看出這個是Spring上下文中基於註解的Servlet上下文,因此,我們最開始的DemoApplication.java
類中宣告的註解@SpringBootApplication
將會被掃描並解析。
步驟9重新整理上下文是最核心的,看過spring原始碼都知道,這個refresh()
方法很經典,具體可以參考小編另一篇文章Spring容器IOC初始化過程
步驟11中會執行整個上下文中,所有實現了ApplicationRunner
和CommandLineRunner
的bean,SpringApplication
第787
程式碼如下:
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的大致啟動過程,有些地方點到為止,並未做深入研究,但我們學習原始碼一為了吸收其編碼精華,寫出更好的程式碼,二為瞭解相關原理,方便更加快速定位解決問題,如有寫的不對的地方,請指正,歡迎評論區留言交流,謝謝大家!