(二)SpringBoot基礎之HelloWorld解析

在這裏插入圖片描述

知其然 知其所以然

創作不易 求點贊👍 求關注❤️ 求分享👥

絮叨

本文是SpringBoot系列的第二篇文章,本篇主要是對第一篇的HelloWorld程序的解析。也會涉及到SpringBoot的一些註解原理的講解。

正文

爲什麼SpringBoot可以使用這麼少的配置,只是寫個主啓動類,在pom文件導入一個springboot依賴,就能啓動一個Web項目呢?爲什麼會這麼神奇呢?接下來我們就基於第一篇的HelloWorld程序去分析下SpringBoot爲什麼可以這麼神奇。
在分析之前,先普及一個名詞starter(翻譯爲啓動器,開胃小吃)。它可以說是SpringBoot中一個接觸最多的一個名詞,可以認爲starter是一種服務的整合——使得使用某個功能的開發者不需要關注各種依賴庫的處理,不需要具體的配置信息,由Spring Boot自動通過classpath路徑下的類發現需要的Bean,並織入bean。

POM解析

首先呢,我們分析一下pom文件。在pom文件中我們導入了一個父項目並且依賴了一個SpringBoot-web的starter。

  1. 父項目spring-boot-starter-parent
    <parent>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>
    
    點進父項目,我們發現父項目又依賴了一個叫做spring-boot-dependencies的項目。
    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.2.5.RELEASE</version>
      <relativePath>../../spring-boot-dependencies</relativePath>
    </parent>
    
    點進這個項目,可以看到它定義了每一個依賴的版本。spring-boot-dependencies這個項目的作用就是管理SpringBoot當前版本的所有依賴的版本。 所以以後我們再導入其他依賴後,只要在SpringBoot的依賴中(基本我們用到的主流依賴都在SpringBoot中集成了),就不需要指定版本號了,它會隨着SpringBoot的版本號,自動導入其對應的版本號。如果沒有在SpringBoot依賴中的管理,就需要我們自己去指定對應的版本號。
    在這裏插入圖片描述
  2. 導入的web依賴
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    我們可以看到,在這裏我們並沒有指定版本號,因爲它在父項目spring-boot-dependencies中聲明瞭版本號,就不需要我們自己去指定了。
    在這裏插入圖片描述
    接下來我們看下spring-boot-starter-web,首先我們可以給他看成兩部分,spring-boot-starterweb
    • spring-boot-starter它是SpringBoot的場景啓動器,每種框架都有自己的啓動器,它很好的降低了使用框架時的複雜度。
    • web標示着它是web的場景啓動器,它會自動導入我們web項目需要用到的依賴。
      在這裏插入圖片描述在這裏插入圖片描述
      Spring Boot將所有的功能場景都抽取出來,做成一個個的starters(啓動器),我們要用什麼功能,就導入對應的starter就可以了,Springboot會自動幫我們導入對應的依賴。

主啓動類解析

  • @SpringBootApplication標註在類上,標明這是一個主配置類,告訴程序這是一個SpringBoot應用。這是一個SpringBoot的約定(SpringBoot的核心理念就是約定優於配置),只要是標註了這個註解,SpringBoot就要運行這個類下面的main方法去啓動這個SpringBoot應用。
    下面我們看一下這個註解都幹了什麼:
    @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 {
    
    我們可以看到@SpringBootApplication是一個組合註解,包括@SpringBootConfiguration,@EnableAutoConfiguration和@ComponentScan
    • @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({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());
        }
        
        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);
               }
           }
        }
        
        由此,我們可以知道,SpringFactoriesLoader.loadFactoryNames()方法得作用是從類路徑或系統資源路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值,將這些值作爲自動配置類導入到容器中。
        那它導入了什麼呢?我們去看一下。在spring.factories中配置的這些XxxxAutoConfiguration自動配置類正好是我們在上面debug中看到的那些。
        在這裏插入圖片描述
        在這裏插入圖片描述
        那自動配置幫我們配置了什麼呢?我們看以下WebMvcAutoConfiguration。它幫我們自動配置了視圖解析器。如果沒有自動配置,我們都要自己去配置視圖解析器。還給我們自動配置了很多的Bean。大家可以自己看一下。
        在這裏插入圖片描述
        總結一下:Spring Boot在啓動的時候從類路徑下或系統資源路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值,將這些值作爲自動配置類導入到容器中,自動配置類就生效,幫我們進行自動配置工作。以前我們需要自己配置的東西,自動配置類都幫我們。爲什麼SpringBoot會有那麼神奇的效果,會需要那麼少的配置,就是因爲導入的這些配置類會自動幫我們配置。
    • @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的更多細節,請看下回分解。

如果本篇博客有任何錯誤,請批評指教,不勝感激 !
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章