前言
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"> * @Configuration * @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"). */
說的內容大概意思如下:
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; }
從源碼我們看出主要做了三件事:
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); } }
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大關時所說:上帝啊,原來那扇門是虛掩着的!
參考
springboot源碼