1. 程式人生 > 程式設計 >走心Springboot原始碼解析: 一、SpringApplication的例項化

走心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

這個類沒有繼承所有任何類,他真的就是一個 啟動類,就相當與寫演演算法題時候的那個main函式,而你的計算流程就寫在其他類或者方法裡面。

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() 的 “ 廣播-事件-監聽器” 的執行過程, 爭取先吃透再解析。

參考連結: spring-boot-2.0.3不一樣系列之原始碼篇