對於源碼學習,我以爲咱們帶着問題一塊兒看會好一點。css
話很少說,咱們首先去[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
類第263
行bash
@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點幫咱們加載了全部依賴的ApplicationListener
和ApplicationContextInitializer
配置項,代碼移步至SpringFactoriesLoader
第132
行,咱們能夠看到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.java
第295
行,代碼以下
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代碼移步至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");
}
}
複製代碼
注意點:
通常狀況下,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的大體啓動過程,有些地方點到爲止,並未作深刻研究,但咱們學習源碼一爲了吸取其編碼精華,寫出更好的代碼,二爲了解相關原理,方便更加快速定位解決問題,若有寫的不對的地方,請指正,歡迎評論區留言交流,謝謝你們!