走心Springboot原始碼解析: 一、SpringApplication的例項化
Springboot原始碼解析:SpringApplication的例項化
打個廣告
個人想寫《springboot原始碼解析》這一系列很久了,但是一直角兒心底的知識積累不足,所以一直沒有動筆。 所以想找一些小夥伴一起寫這一系列,互相糾錯交流學習。
如果有小夥伴有興趣一起把這一系列的講解寫完的話,加下我微信:13670426148,我們一起完成,當交流學習。
後期還想寫一系列介紹rpc框架的,不過要再過一陣子了,先把springboot的寫完
前言
這系列的教程從 Springboot專案的入口開始,即 SpringApplication.run(Application.class,args) 開始進行講解。
啟動入口
先貼一下入口類的程式碼:
@SpringBootApplication
//@EnableTransactionManagement
@EnableAsync
@EnableAutoConfiguration(exclude = {DataSourceAutoConfiguration.class})
@EnableScheduling
@EnableRetry
@ComponentScan(basePackages = {"*** ","***"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class,args);
}
}
複製程式碼
其中,入口類的類名是 Application,這個類的型別將作為引數,傳遞給 SpringApplication的 run() 方法,還有一些初始化引數,這些都在run()方法的時候會進行處理,可以先記住他們。
現在可以記住 @EnableAutoConfiguration
和 @EnableScheduling
和 @ComponentScan
等註解,記住這些註解,後面將介紹其執行過程。
SpringApplication 例項化過程
Application
SpringApplication用於從java main方法引導和啟動Spring應用程式,預設情況下,將執行以下步驟來引導我們的應用程式:
- 建立一個恰當的ApplicationContext例項(取決於類路徑)
- 註冊CommandLinePropertySource,將命令列引數公開為Spring屬性
- 重新整理應用程式上下文,載入所有單例bean
- 觸發全部CommandLineRunner bean
大多數情況下,像SpringApplication.run(ShiroApplication.class,args);這樣啟動我們的應用,也可以在執行之前建立和自定義SpringApplication例項,具體可以參考註釋中示例。
SpringApplication可以從各種不同的源讀取bean。 通常建議使用單個@Configuration類來引導,但是我們也可以通過以下方式來設定資源:
- 通過AnnotatedBeanDefinitionReader載入完全限定類名
- 通過XmlBeanDefinitionReader載入XML資源位置,或者是通過GroovyBeanDefinitionReader載入groovy指令碼位置
- 通過ClassPathBeanDefinitionScanner掃描包名稱
- 也就是說SpringApplication還是做了不少事的,具體實現後續會慢慢講來,今天的主角只是SpringApplication構造方法。
public class SpringApplication{
public SpringApplication(ResourceLoader resourceLoader,Object... sources) {
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.headless = true;
this.registerShutdownHook = true;
//todo -------------------------------------------------------
this.additionalProfiles = new HashSet();
//上面的資訊都不是主要的,主要的資訊在這裡,在這裡進行
//(1)執行環境 (2) 例項化器 (3)監聽器等的初始化過程,下面將詳細解析
this.initialize(sources);
}
public ConfigurableApplicationContext run(String... args) {
*******
}
}
複製程式碼
這個 this.initialize(sources) 方法還是在 SpringApplication裡面的,所以這個SpringApplication真的是貫穿springboot整個啟動過程的一個類,後面還有一個run() 方法。
我們來看 initialize(Object[] sources) 方法的內容
private void initialize(Object[] sources) {
//sources裡面就是我們的入口類: Application.class
if (sources != null && sources.length > 0) {
this.sources.addAll(Arrays.asList(sources));
}
//這行程式碼設定SpringApplication的屬性webEnvironment,deduceWebEnvironment方法是推斷是否是web應用的核心方法
this.webEnvironment = this.deduceWebEnvironment();
//獲取所有的例項化器Initializer.class this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//獲取所有的監聽器
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
//這個不解釋了,就是我們的Application.class ,我們寫的入口類,過程就是從當前的堆疊中找到我們寫main方法額類,就是獲取我們的入口類了
this.mainApplicationClass = this.deduceMainApplicationClass();
}
複製程式碼
下面就解釋3個部分的具體實現:
(1) 推測執行環境
(2)獲取所有的例項化器Initializer.class
又展示了其獲取過程
(3)獲取所有的監聽器Initializer.class
推測執行環境
推測執行環境,並賦予個 this.webEnvironment 這個屬性,deduceWebEnvironment方法是推斷是否是web應用的核心方法。
在後面SpringApplication 的run()方法中建立 ApplicationContext 的時候就是根據webEnvironment這個屬性來判斷是 AnnotationConfigEmbeddedWebApplicationContext
還是 AnnotationConfigApplicationContext
程式碼如下:
private boolean deduceWebEnvironment() {
String[] var1 = WEB_ENVIRONMENT_CLASSES;
int var2 = var1.length;
for(int var3 = 0; var3 < var2; ++var3) {
String className = var1[var3];
if (!ClassUtils.isPresent(className,(ClassLoader)null)) {
return false;
}
}
return true;
}
WEB_ENVIRONMENT_CLASSES = new String[]{
"javax.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext"
};
複製程式碼
推斷過程很簡單,不過我不理解為什麼這麼寫,因為我這個是web專案,所以 this.webEnvironment
的值為true
ClassUtils.isPresent()的過程其實很簡單,就是判斷 WEB_ENVIRONMENT_CLASSES
裡面的兩個類能不能被載入,如果能被載入到,則說明是web專案,其中有一個不能被載入到,說明不是。
獲取所有的例項化器Initializer.class
//看完這個方法真覺得很棒,獲取工廠例項
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type) {
return this.getSpringFactoriesInstances(type,new Class[0]);
}
//記住 ty
private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,Class<?>[] parameterTypes,Object... args) {
//這個是獲取類載入器
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
//type是ApplicationContextInitializer.class,獲取型別工廠的名字
Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type,classLoader));
//獲取工廠例項
List<T> instances = this.createSpringFactoriesInstances(type,parameterTypes,classLoader,args,names);
//排序,按照@Order的順序進行排序,沒有@Order的話,就按照原本的順序進行排序,不管他問題不大
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
複製程式碼
獲取指定型別工廠的名字
public static List<String> loadFactoryNames(Class<?> factoryClass,ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
try {
//獲取所有 jar包下面的 META-INF/spring.factories 的urls
Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
ArrayList result = new ArrayList();
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
//每個spring.factories裡的下的內容裝載成Properties資訊
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
//下面內容會繼續解析
String factoryClassNames = properties.getProperty(factoryClassName);
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
} catch (IOException var8) {
throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]",var8);
}
}
複製程式碼
圖1.1如下:
還有很多,就不一一列舉出來了
(1)就是找到所有的 /MEIT-INF下面的spring.factory
(2)轉換成 properties,
(3)properties.getProperty(factoryClassName)
關於 /MEIT-INF/spring.factory,不知道大家有沒有寫過 starter,如果不知道是什麼,很多依賴比如mybatis-plus 、springboot的包裡面都有很多依賴,打成starter的形式,被我們springboot專案依賴。
可以查一查springboot自定義starter,看一下,大概就知道一個spring.factory的作用了。。
此時 factoryClassName 相當於是一個key獲取對應的properties裡面是否有 "org.springframework.context.ApplicationContextInitializer"對應的值,有的話,新增到result中
到最後可以得到的有,即圖1.2
獲取工廠例項(根據類名)
再進行 獲取工廠例項 操作,步驟很簡單,就是用構造器和類名生成指定的 Inializer,到現在的過程都很簡單。
貼個程式碼,不進行解釋了
private <T> List<T> createSpringFactoriesInstances(Class<T> type,ClassLoader classLoader,Object[] args,Set<String> names) {
List<T> instances = new ArrayList(names.size());
Iterator var7 = names.iterator();
while(var7.hasNext()) {
String name = (String)var7.next();
try {
Class<?> instanceClass = ClassUtils.forName(name,classLoader);
Assert.isAssignable(type,instanceClass);
Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
T instance = BeanUtils.instantiateClass(constructor,args);
instances.add(instance);
} catch (Throwable var12) {
throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name,var12);
}
}
return instances;
}
複製程式碼
獲取所有的監聽器Initializer.class
類似的上面的步驟,監聽器的獲得結果如下: 圖1.3
總結
(1)還記得SpringApplication.class有那些屬性嗎
public class SpringApplication{
private List<ApplicationContextInitializer<?>> initializers; //如圖1.2這個是拿6個Initializer
private WebApplicationType webApplicationType; //這個是true
private List<ApplicationListener<?>> listeners; //這和是圖1.3的10個Listener
private Class<?> mainApplicationClass; //結果就是DemoApplication
//另外還有構造方法設定的值
public SpringApplication(ResourceLoader resourceLoader,Object... sources) {
this.bannerMode = Mode.CONSOLE;
this.logStartupInfo = true;
this.addCommandLineProperties = true;
this.headless = true;
this.registerShutdownHook = true;
//todo -------------------------------------------------------
this.additionalProfiles = new HashSet();
//上面的資訊都不是主要的,主要的資訊在這裡,在這裡進行
//(1)執行環境 (2) 例項化器 (3)監聽器等的初始化過程,下面將詳細解析
this.initialize(sources);
}
}
複製程式碼
(2) SpringApplication.class 就是一個操作啟動過程的類
例項化過程就是載入一些最初始的引數和資訊,比如監聽器,例項化器,bannerMode,additionalProfiles等資訊。其中最主要的還是監聽器和例項化器, 關於監聽器,是springboot啟動過程最重要的一部分,其啟動過程的機制大概就是, 用一個廣播,他廣播一些event事件,然後這些監聽器(10個),就會根據這些事件,做不同的反應。 監聽器模式大家可以先了解一下。
下期預告
下面會講 SpringApplication.run() 裡面的內容了,主要run() 的 “ 廣播-事件-監聽器” 的執行過程, 爭取先吃透再解析。