spring-boot-2.0.3啟動源碼篇一 - SpringApplication構造方法


前言

  spring-boot-2.0.3應用篇 - shiro集成,實現了spring-boot與shiro的整合,效果大家也看到了,工程確實集成了shiro的認證與授權功能。如果大家能正確搭建起來,並達到了認證和授權的效果,那說明我們會用了,說明我們知其然了;很好,能滿足工作中的基本要求了。

  但是這樣就夠了嗎?很顯然還是不夠的,知其然而不知其所以然是一道瓶頸,如果我們能跨過這道瓶頸,后面的路會越來越坦盪。就拿上篇博客來講,我們僅僅只是在ShiroConfig類中加入了幾個bean配置,怎么就讓spring-boot集成了shiro,shiro又是如何做到認證和授權的,等等一些列問題,如果我們去細想的話,真的有很多疑點需要我們去探索。

  既然我們要去探索,勢必就要讀源碼了。源碼確實不好讀,在我們工作當中,當我們讀同事(或者前同事)寫的代碼的時候,總有那么一句話:草泥馬,這是哪個sb寫的,縈繞在我們的心頭,甚至有時候會發現,這他么是我自己寫的啊,哎,我操!有時候讀自己寫的代碼都頭疼,更別說看別人寫的了。

  說了那么多,我們切入到正題,接下來會有一系列的文章來解析springboot的啟動過程,而今天我們只看SpringApplication類的構造方法。

SpringApplication類

  入口還是那個熟悉的入口:main函數

  SpringApplication類注釋

/**
 * Class that can be used to bootstrap and launch a Spring application from a Java main
 * method. By default class will perform the following steps to bootstrap your
 * application:
 *
 * <ul>
 * <li>Create an appropriate {@link ApplicationContext} instance (depending on your
 * classpath)</li>
 * <li>Register a {@link CommandLinePropertySource} to expose command line arguments as
 * Spring properties</li>
 * <li>Refresh the application context, loading all singleton beans</li>
 * <li>Trigger any {@link CommandLineRunner} beans</li>
 * </ul>
 *
 * In most circumstances the static {@link #run(Class, String[])} method can be called
 * directly from your {@literal main} method to bootstrap your application:
 *
 * <pre class="code">
 * &#064;Configuration
 * &#064;EnableAutoConfiguration
 * public class MyApplication  {
 *
 *   // ... Bean definitions
 *
 *   public static void main(String[] args) throws Exception {
 *     SpringApplication.run(MyApplication.class, args);
 *   }
 * }
 * </pre>
 *
 * <p>
 * For more advanced configuration a {@link SpringApplication} instance can be created and
 * customized before being run:
 *
 * <pre class="code">
 * public static void main(String[] args) throws Exception {
 *   SpringApplication application = new SpringApplication(MyApplication.class);
 *   // ... customize application settings here
 *   application.run(args)
 * }
 * </pre>
 *
 * {@link SpringApplication}s can read beans from a variety of different sources. It is
 * generally recommended that a single {@code @Configuration} class is used to bootstrap
 * your application, however, you may also set {@link #getSources() sources} from:
 * <ul>
 * <li>The fully qualified class name to be loaded by
 * {@link AnnotatedBeanDefinitionReader}</li>
 * <li>The location of an XML resource to be loaded by {@link XmlBeanDefinitionReader}, or
 * a groovy script to be loaded by {@link GroovyBeanDefinitionReader}</li>
 * <li>The name of a package to be scanned by {@link ClassPathBeanDefinitionScanner}</li>
 * </ul>
 *
 * Configuration properties are also bound to the {@link SpringApplication}. This makes it
 * possible to set {@link SpringApplication} properties dynamically, like additional
 * sources ("spring.main.sources" - a CSV list) the flag to indicate a web environment
 * ("spring.main.web-application-type=none") or the flag to switch off the banner
 * ("spring.main.banner-mode=off").
 */
View Code

    說的內容大概意思如下:

    SpringApplication用於從java main方法引導和啟動Spring應用程序,默認情況下,將執行以下步驟來引導我們的應用程序:

      1、創建一個恰當的ApplicationContext實例(取決於類路徑)

      2、注冊CommandLinePropertySource,將命令行參數公開為Spring屬性

      3、刷新應用程序上下文,加載所有單例bean

      4、觸發全部CommandLineRunner bean

    大多數情況下,像SpringApplication.run(ShiroApplication.class, args);這樣啟動我們的應用,也可以在運行之前創建和自定義SpringApplication實例,具體可以參考注釋中示例。

    SpringApplication可以從各種不同的源讀取bean。 通常建議使用單個@Configuration類來引導,但是我們也可以通過以下方式來設置資源:

      1、通過AnnotatedBeanDefinitionReader加載完全限定類名

      2、通過XmlBeanDefinitionReader加載XML資源位置,或者是通過GroovyBeanDefinitionReader加載groovy腳本位置

      3、通過ClassPathBeanDefinitionScanner掃描包名稱

    也就是說SpringApplication還是做了不少事的,具體實現后續會慢慢講來,今天的主角只是SpringApplication構造方法。

  SpringApplication構造方法

    源代碼如下

/**
     * Create a new {@link SpringApplication} instance. The application context will load
     * beans from the specified primary sources (see {@link SpringApplication class-level}
     * documentation for details. The instance can be customized before calling
     * {@link #run(String...)}.
     * @param resourceLoader the resource loader to use
     * @param primarySources the primary bean sources
     * @see #run(Class, String[])
     * @see #setSources(Set)
     */
    @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));
        this.webApplicationType = deduceWebApplicationType();
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = deduceMainApplicationClass();
    }

    從注釋上來看,就是說創建一個ShiroApplication實例,應用上下文從特定的資源文件中加載bean。可以在調用run之前自定義實例。

    從源碼上來看,主要是deduceWebApplicationType();getSpringFactoriesInstances(xxx.class);deduceMainApplicationClass();這三個方法,我們一個一個來看。

    deduceWebApplicationType

      推斷web應用類型

private WebApplicationType deduceWebApplicationType() {
    if (ClassUtils.isPresent(REACTIVE_WEB_ENVIRONMENT_CLASS, null)
            && !ClassUtils.isPresent(MVC_WEB_ENVIRONMENT_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : WEB_ENVIRONMENT_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

// 判斷給定的類是否能夠加載,就是說類路徑下是否存在給定的類
public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
    try {
        forName(className, classLoader);
        return true;
    }
    catch (Throwable ex) {
        // Class or one of its dependencies is not present...
        return false;
    }
}

      如果org.springframework.web.reactive.DispatcherHandler能夠被加載且org.springframework.web.servlet.DispatcherServlet不能夠被加載,那么斷定web應用類型是REACTIVE;如果javax.servlet.Servlet和org.springframework.web.context.ConfigurableWebApplicationContext任意一個不能被加載,那么斷定web應用類型是NONE;如果不能斷定是REACTIVE和NONE,那么就是SERVLET類型;具體這三種類型代表什么含義,大家可以查看WebApplicationType中的說明。  

    getSpringFactoriesInstances

      從字面意思看就是獲取spring工廠實例,至於從哪獲取哪些工廠實例,我們往下看。

      getSpringFactoriesInstances源碼

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<>(
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));            // 獲取指定類型的工廠名字
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,    // 根據名字、類型創建工廠實例
            classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}
View Code

      從源碼我們看出主要做了三件事:

        1、loadFactoryNames,加載指定類型的工廠名稱

          loadSpringFactories

          loadSpringFactories源碼

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));    // classLoader.getResources(FACTORIES_RESOURCE_LOCATION)獲取類路徑下全部的META-INF/spring.factories的URL
        result = new LinkedMultiValueMap<>();
        while (urls.hasMoreElements()) {                                        // 遍歷全部的URL,逐個讀取META-INF/spring.factories中的屬性
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                List<String> factoryClassNames = Arrays.asList(
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
                result.addAll((String) entry.getKey(), factoryClassNames);        // 屬性全部放入MultiValueMap<String, String> result中,注意result的類型
            }
        }
        cache.put(classLoader, result);                                            // 結果放入緩存,方便下次查找
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}
View Code       

          loadSpringFactories做了以下這些事

            a、  查找類路徑下全部的META-INF/spring.factories的URL

            b、 根據url加載全部的spring.factories中的屬性,spring.factories內容如下

            c、  將所有spring.factories中的值緩存到SpringFactoriesLoader的cache中:

              private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();方便下次調用。

          加載完所有的工廠名稱之后,然后從中獲取指定工廠類型的工廠名稱列表,也就是getOrDefault(factoryClassName, Collections.emptyList())做的事。

        2、createSpringFactoriesInstances,創建指定類型的工廠實例

          根據上面獲取的指定類型的工廠名稱列表來實例化工廠bean,我們可以簡單的認為通過反射來實例化,但是具體的實現也沒那么簡單,感興趣的小伙伴可以自己去跟。

        3、對工廠實例進行排序,然后返回排序后的實例列表

          排序規則:@Order從小到大排序,沒有order則按沒排序之前的順序。

      deduceMainApplicationClass

        從當前堆棧跟蹤列表中獲取main方法所在的類名   

    構造方法總結

      1、 構造自身實例

      2、 推測web應用類型,並賦值到屬性webApplicationType

      3、 設置屬性List<ApplicationContextInitializer<?>> initializers和List<ApplicationListener<?>> listeners

        中途讀取了類路徑下所有META-INF/spring.factories的屬性,並緩存到了SpringFactoriesLoader的cache緩存中

      4、 推斷主類,並賦值到屬性mainApplicationClass

      構造方法完成之后,實例的屬性值如下

感想

  本來是想着springboot啟動源碼解析只用兩篇來說明的,之后講shiro的源碼;可我寫着寫着發現好多實例莫名奇妙的就被實例化了,很多細節沒有讀到,所以決定細摳,將springboot的啟動過程拆分成多篇來講解,真真正正的明白springboot在啟動的過程都做了些什么。

  補充一句:有時候,不是對手有多強大,只是我們不敢去嘗試;勇敢踏出第一步,你會發現自己比想象中更優秀!誠如海因斯第一次跑進人類10s大關時所說:上帝啊,原來那扇門是虛掩着的!

參考

  spring boot 2.0 源碼分析(一)

  springboot源碼


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM