知其然 知其所以然
創作不易 求點贊👍 求關注❤️ 求分享👥
絮叨
本文是SpringBoot系列的第二篇文章,本篇主要是對第一篇的HelloWorld程序的解析。也會涉及到SpringBoot的一些註解原理的講解。
正文
爲什麼SpringBoot可以使用這麼少的配置,只是寫個主啓動類,在pom文件導入一個springboot依賴,就能啓動一個Web項目呢?爲什麼會這麼神奇呢?接下來我們就基於第一篇的HelloWorld程序去分析下SpringBoot爲什麼可以這麼神奇。
在分析之前,先普及一個名詞starter(翻譯爲啓動器,開胃小吃)。它可以說是SpringBoot中一個接觸最多的一個名詞,可以認爲starter是一種服務的整合——使得使用某個功能的開發者不需要關注各種依賴庫的處理,不需要具體的配置信息,由Spring Boot自動通過classpath路徑下的類發現需要的Bean,並織入bean。
POM解析
首先呢,我們分析一下pom文件。在pom文件中我們導入了一個父項目並且依賴了一個SpringBoot-web的starter。
- 父項目spring-boot-starter-parent
點進父項目,我們發現父項目又依賴了一個叫做spring-boot-dependencies的項目。<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.2.5.RELEASE</version> </parent>
點進這個項目,可以看到它定義了每一個依賴的版本。spring-boot-dependencies這個項目的作用就是管理SpringBoot當前版本的所有依賴的版本。 所以以後我們再導入其他依賴後,只要在SpringBoot的依賴中(基本我們用到的主流依賴都在SpringBoot中集成了),就不需要指定版本號了,它會隨着SpringBoot的版本號,自動導入其對應的版本號。如果沒有在SpringBoot依賴中的管理,就需要我們自己去指定對應的版本號。<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.2.5.RELEASE</version> <relativePath>../../spring-boot-dependencies</relativePath> </parent>
- 導入的web依賴
我們可以看到,在這裏我們並沒有指定版本號,因爲它在父項目spring-boot-dependencies中聲明瞭版本號,就不需要我們自己去指定了。<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>
接下來我們看下spring-boot-starter-web,首先我們可以給他看成兩部分,spring-boot-starter和web。- spring-boot-starter它是SpringBoot的場景啓動器,每種框架都有自己的啓動器,它很好的降低了使用框架時的複雜度。
- web標示着它是web的場景啓動器,它會自動導入我們web項目需要用到的依賴。
Spring Boot將所有的功能場景都抽取出來,做成一個個的starters(啓動器),我們要用什麼功能,就導入對應的starter就可以了,Springboot會自動幫我們導入對應的依賴。
主啓動類解析
- @SpringBootApplication標註在類上,標明這是一個主配置類,告訴程序這是一個SpringBoot應用。這是一個SpringBoot的約定(SpringBoot的核心理念就是約定優於配置),只要是標註了這個註解,SpringBoot就要運行這個類下面的main方法去啓動這個SpringBoot應用。
下面我們看一下這個註解都幹了什麼:
我們可以看到@SpringBootApplication是一個組合註解,包括@SpringBootConfiguration,@EnableAutoConfiguration和@ComponentScan@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} ) public @interface SpringBootApplication {
-
@SpringBootConfiguration標註當前類是一個SpringBoot配置類。就像之前我們在xml中去配置一樣,只不過在這裏我們用類去代替xml文件。
@SpringBootConfiguration底層被@Configuration註解標註,表明這是一個配置類,它會被@ComponentScan掃描到,也可以說被@SpringBootConfiguration標註的類是容器中的一個組件。@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration {
-
@EnableAutoConfiguration開啓自動配置。SpringBoot根據我們添加的jar包來配置項目的默認配置。以前我們導入jar要自己配置一些屬性,而現在SpringBoot幫我們把這些配置好了。
那SpringBoot是怎麼幫我們去配置的呢,請大家繼續往下看@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage @Import({AutoConfigurationImportSelector.class}) public @interface EnableAutoConfiguration {
- @AutoConfigurationPackage:自動配置包。
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @Import({Registrar.class}) public @interface AutoConfigurationPackage {
- @Import({Registrar.class}):Spring的底層註解@Import,給容器中導入一個Registrar.class組件。該組件將主配置類(@SpringBootApplication標註的類)的所在包及下面所有子包裏面的所有組件掃描到Spring容器。
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports{ Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // registry就是保存要註冊到容器中的組件的Bean定義.也就是保存主配置類的所在包及下面所有子包裏面的所有Bean定義和一些默認的組件。 // (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()就是主配置類的所在包及下面所有子包的路徑 AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()); } public Set<Object> determineImports(AnnotationMetadata metadata) { return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata)); } }
所以這也就是爲什麼主啓動類要寫在包外面的原因:因爲@SpringBootApplication只會掃描@SpringBootApplication註解標記類包下及其子包的類(特定註解標記,比如說@Controller,@Service,@Component,@Configuration和@Bean註解等等)納入到spring容器,如果我們定義的Bean不在@SpringBootApplication註解標記類相同包下及其子包的類,所以需要我們去配置一下掃包路徑。
- @Import({Registrar.class}):Spring的底層註解@Import,給容器中導入一個Registrar.class組件。該組件將主配置類(@SpringBootApplication標註的類)的所在包及下面所有子包裏面的所有組件掃描到Spring容器。
- @Import({AutoConfigurationImportSelector.class}):Spring的底層註解@Import,給容器中導入一個AutoConfigurationImportSelector.class組件。該組件是我們要導入哪些組件的選擇器。它會將所有需要導入的組件以全類名的方式存到一個AutoConfigurationImportSelector.AutoConfigurationEntry類型的內部類中返回。之後這些組件就會被添加到spring容器中。
我們在AutoConfigurationImportSelector.getAutoConfigurationEntry()方法上添加一個斷點並以debug方式啓動主啓動類。package org.springframework.boot.autoconfigure; public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered { protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) { // annotationMetadata是註解的元信息,包括註解的類的全路徑名 if (!this.isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } else { AnnotationAttributes attributes = this.getAttributes(annotationMetadata); // 根據標註類的元信息和屬性得到一個configurations的List集合,這個configurations保存的就是我們在容器中要導入的組件。 List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); // 接下來都是對configurations的一些處理,比如去重、去掉一些不包括的組件等等,最終返回一個AutoConfigurationImportSelector.AutoConfigurationEntry類型的對象。 configurations = this.removeDuplicates(configurations); Set<String> exclusions = this.getExclusions(annotationMetadata, attributes); this.checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = this.filter(configurations, autoConfigurationMetadata); this.fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions); } } }
也就是說AutoConfigurationImportSelector給容器中導入非常多的自動配置類,就是給容器中導入這個場景需要的所有組件,並配置好這些組件。我們也可以看到他的所有自動配置都是叫XxxxAutoConfiguration,這也是SpringBoot的一個約定,我的自動配置類都是叫XxxxAutoConfiguration。以後我們要是找某場景的自動配置,就可以搜XxxxxAutoConfiguration就可以找到了。
由上面代碼可以知道最重要的就是這一行,之後都是對這個結果進行處理的。接下來我們看看這個方法到底做了什麼。List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes); protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { // 這個configurations實際由這個方法得到,這個方法有兩個參數,第一個參數是EnableAutoConfiguration對象,第二個參數是類加載器。 List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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; }
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) { String factoryTypeName = factoryType.getName(); // 調用loadSpringFactories()方法,參數是classLoader。 // 後面的getOrDefault()方法是把loadSpringFactories()方法的返回結果進行判斷, // 如果Map集合中有這個factoryTypeName指定的key時,就使用這個key值,返回key對應的value。 // 如果沒有就返回默認值Collections.emptyList() // 把結果轉換成List返回。 return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList()); }
由此,我們可以知道,SpringFactoriesLoader.loadFactoryNames()方法得作用是從類路徑或系統資源路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值,將這些值作爲自動配置類導入到容器中。private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) { MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader); if (result != null) { return result; } else { try { // 從類路徑或系統資源路徑下的META-INF/spring.factories中獲取資源 Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories"); LinkedMultiValueMap result = new LinkedMultiValueMap(); while(urls.hasMoreElements()) { // 循環上面獲取到的資源,轉化成Properties資源 URL url = (URL)urls.nextElement(); UrlResource resource = new UrlResource(url); Properties properties = PropertiesLoaderUtils.loadProperties(resource); // 轉化成Set集合遍歷 Iterator var6 = properties.entrySet().iterator(); while(var6.hasNext()) { // 得到key和value,對value做一些處理後,把key和value保存到LinkedMultiValueMap對象中返回 Entry<?, ?> entry = (Entry)var6.next(); String factoryTypeName = ((String)entry.getKey()).trim(); String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue()); int var10 = var9.length; for(int var11 = 0; var11 < var10; ++var11) { String factoryImplementationName = var9[var11]; result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException var13) { throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13); } } }
那它導入了什麼呢?我們去看一下。在spring.factories中配置的這些XxxxAutoConfiguration自動配置類正好是我們在上面debug中看到的那些。
那自動配置幫我們配置了什麼呢?我們看以下WebMvcAutoConfiguration。它幫我們自動配置了視圖解析器。如果沒有自動配置,我們都要自己去配置視圖解析器。還給我們自動配置了很多的Bean。大家可以自己看一下。
總結一下:Spring Boot在啓動的時候從類路徑下或系統資源路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值,將這些值作爲自動配置類導入到容器中,自動配置類就生效,幫我們進行自動配置工作。以前我們需要自己配置的東西,自動配置類都幫我們。爲什麼SpringBoot會有那麼神奇的效果,會需要那麼少的配置,就是因爲導入的這些配置類會自動幫我們配置。
- @AutoConfigurationPackage:自動配置包。
-
@ComponentScan掃描當前包及其子包下被@Component,@Controller,@Service,@Repository註解標記的類並納入到spring容器中進行管理。
-
總結
本篇我們基於第一篇的HelloWorld程序對SpringBoot可以簡單方便快速的搭建一個Web項目的解析。
從pom文件和主啓動類進行了分析。pom文件中我們依賴SpringBoot的starters(啓動器),只需要在項目裏面引入這些starter相關場景的所有依賴都會導入進來。
主啓動類我們基於@SpringBootApplication註解進行分析,因爲包含@SpringBootConfiguration註解,所以這是一個配置類,它會被@ComponentScan掃描到。因爲包含@EnableAutoConfiguration註解,所以會把主配置類的所在包及下面所有子包裏面的所有組件掃描到Spring容器。所以SpringBoot在啓動的時候從類路徑下或系統資源路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的配置類,將這些值作爲自動配置類導入到容器中,我們就不需要像以前一樣去配置。因爲包含@ComponentScan,所以它會掃描當前包及其子包下被@Component,@Controller,@Service,@Repository註解標記的類並納入到spring容器中進行管理。
想要了解SpringBoot的更多細節,請看下回分解。
如果本篇博客有任何錯誤,請批評指教,不勝感激 !