1. 程式人生 > >SpringBoot-自動裝配物件及原始碼ImportSelector分析

SpringBoot-自動裝配物件及原始碼ImportSelector分析

 

SpringBoot框架已經很流行了,筆者做專案也一直在用,使用久了,越來越覺得有必要理解SpringBoot框架中的一些原理了,目前的面試幾乎都會用問到底層原理。我們在使用過程中基本上是搭建有一個框架拿來現用,在此過程中遇到問題就去百度來解決相應的問題,但是,對其原理不理解的情況下,雖然問題能夠解決,還是不會有多大收穫。下次再遇到問題的時候仍感覺力不從心。在瞭解了相關問題及解決方案之後,筆者總結了一些原理,這裡作為學習筆記,與大家共勉。

一、Springboot環境搭建

  這裡我是用的環境及開發工具是JDK8+IntelliJIDEA

  (1)建立Spring Boot專案

  

 

名稱根據需要進行更改。

輸入專案名稱或預設,點Finish。至此,SpringBoot工程建立完畢,結構如下:

二、Spring Boot使用

  工程建立完畢之後,我們在專案中建立一個類:Animal,

那麼如果想要把這個類交給Spring去管理,怎麼辦呢?使用過SpringBoot的人都會,我們通過Animal類上面加上@component註解

加了這個註解之後,Spring在啟動的時候就會掃描該類,完成初始化,並把它放入Spring容器中,啟動,看效果如下:

 

  非常簡單的方式就實現了,僅僅是通過一個註解,那麼,這個Animal類,通過這樣一個註解是怎麼交給Spring去管理的呢,中間的過程經歷了什麼呢?內部又是怎樣的一種機制呢?為了看到它的實現過程,這裡面主要是兩個註解,@SpringBootApplication 和@EnableAutoConfiguration。這兩個註解都是複合註解。

2.1 @SpringBootApplication實現原理

  點選進去看@SpringBootApplication的實現過程:

這裡面有很多註解,首先來看@SpringBootConfiguration這個註解,從字面來看,他應該是一個配置註解,通過實現過程,我們知道,他應該就是Spring提供的一個配置註解。

那麼,這個註解有什麼作用呢?其實我們也用到過,來演示一下他的使用。首先,建立一個config包,然後我們在包裡建立兩個類:User,Car,如圖:

這兩個類沒有加入任何註解,我們在MyConfig這個配置類中,通過手動裝配的方式,進行類的初始化,即使用@Configuration這個註解和@Bean註解的方式,啟動專案執行,效果如下:如圖:

  

通過這樣的方式,我們照樣可以將自定義Bean交給Spring容器去管理。這裡需要注意的是config這個包一定要和啟動類在同一個資料夾下,否則,不加指定掃描的包,Spring預設是不會掃描到的。其實,將@Configuration換成@SpringBootConfiguration效果是一樣的,因為後者是一個複合註解,只不過是多包裝了一層而已。那麼該註解的作用就很明顯了,就是將@configuration註解下面所有帶有Bean註解的物件進行裝配就交給Spring容器去管理。這就是SpringBoot框架自動裝配的一部分。SpringBoot在啟動的時候,會將很多的Bean進行自動裝配,通過什麼方式呢?開啟原始碼:

我們看到,其實這裡面配置了很多自動裝配的類,當我們啟動服務的時候,他會掃描這個配置檔案當中的所有配置項進行自動裝配,這裡麵包含我們幾乎能用到的所有元件、包括Redis、Elasticsearch、JDBC等。但是我們知道SpringBoot在啟動的時候並不會把所有的類都一起初始化加入到容器當中,這個是有前提條件的,我們隨便點進去一個:

我們注意到,哪個類會被自動裝配是會有條件的,從@ConditionalOnClass來看,條件就是該類存在,並且在Spring容器中存在例項的情況下才會進行裝配進而執行裝配類。比如說,我在專案中用到了Elastic search,那麼我們通過@Configuration下的@Bean配置就會載入該例項,這是一個實際應用:

2.2 @EnableAutoConfiguration實現原理

  從上面的分析可知,我們啟動類中的@EnableAutoConfiguration註解它會將所有滿足條件的Bean進行自動裝配。那麼問題又來了,@EnableAutoConfiguration它又是如何實現的呢?繼續看實現過程:

即,通過@import將所有SpringBoot需要裝配的類匯入進來。那麼@Import該如何理解呢,來看個例子,我們把MyConfig中的@Configuration註解去掉,這時候,啟動專案,User類和Car類都不會被載入

 

但是,我們還想使用這兩個類,怎麼辦呢?通過@Import註解來實現。首先我們看@Import它的實現過程:需要傳入Class型別的引數,是一個數組,那麼在啟動類中加入這個註解看一下執行效果:

通過這種方式,同樣,實現了Bean的初始化。那麼這樣的原理是什麼呢?也就是說SpringBoot在啟動的時候會預設拿到和啟動類在同一級的資料夾(包路徑),然後對其所有帶有註解的類進行掃描,接著,使用AutoConfigurationImportSelector.class這個類載入,該類實現了DeferredImportSelector,DeferredImportSelector繼承了ImportSelector,這就是自動匯入的原理了。

 這個類中有一個方法:selectImports,該方法返回一個String型別的陣列,該陣列中放的就是自動裝配進來的包路徑了,作用找到滿足配置的所有帶有註解的類,然後交由Spring去管理。

 

瞭解了這個原理之後,那麼,我們就可以自己來實現把某一些類交給Spring去管理的方法了。

首先,去掉配置類中的@Configuration註解以及類中的@compoent註解,這時候執行專案,我們的自定義類是無法交給Spring去管理的,接下來,我們自己來實現,模擬SpringBoot的類載入過程:

然後,建立自定義類ModelImportSelector並實現ImportSelector介面並重寫selectImports方法,重寫該方法的目的就是把返回結果中的每一個元素,即類的全名稱,交給Spring容器去管理。我們放兩個類進去,分別是

 Animal,Car,然後在啟動類中通過import註解引入,過程如下:

public class ModelImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{
                "com.springboot.demo.model.Animal",
                "com.springboot.demo.model.Car"
        };
    }
}
View Code
@SpringBootApplication
@Import(ModelImportSelector.class)
public class DemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run( DemoApplication.class, args );
        context.close();
    }
}
View Code

執行結果:

這種方式實現了將我們想要交給Spring管理的類進行了託管,但是,如果有N多個類的話,這種寫法會累死的,因此,需要通過遞迴的方式載入包路徑名,然後統一初始化,將該包路徑下的所有類進行溼例項化。首先我們自己建立一個註解:MyImport,並加入Spring的@Import註解,然後將啟動類中的@Import註解改成我們自定義註解。

目的:掃描 "com.springboot.demo.model"下面的所有類,初始化並交給Spring容器管理

自定義註解:

**
 * Created by ${USRE}  on 2019/6/20 on 14:46
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ModelImportSelector.class)
public @interface MyImport {
    String [] packages();
}
View Code

啟動類加入自定義註解:

@SpringBootApplication
@MyImport(packages = "com.springboot.demo.model")
public class DemoApplication {

    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run( DemoApplication.class, args );
        context.close();
    }
View Code

重寫selectImports方法:

private List<String> classList=new ArrayList <>();
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {

        //通過位元組碼檔案解讀,獲取元素據信息,通過註解名稱,返回註解所對應的屬性的Map集合
        String [] packages = (String [])annotationMetadata.getAnnotationAttributes( MyImport.class.getName() ).get( "packages" );
        if (packages == null) {
            return null;
        }
        scanPackagesRecursion(packages);
        //獲取註解中packages中配置的內容
        //獲取需要掃描的包的所有類的路徑
        return !classList.isEmpty()?classList.toArray( new String[classList.size()] ):null;
    }

    private void scanPackagesRecursion(String [] packages){
        for(String path : packages){
            doScanPackages(path);
        }
    }

    /**
     * 方法遞迴
     * @param path
     */
    private void doScanPackages(String path){
        URL resource = this.getClass().getClassLoader().
                getResource( path.replaceAll( "\\.", "/" ) );
        File file = new File( resource.getFile() );
        File[] files=file.listFiles();
        for (File fileSub : files) {
            if(fileSub.isDirectory()){
                doScanPackages(path+"."+fileSub.getName());
            }else{
                String fileName=fileSub.getName();
                System.out.println("fileName:"+fileName);
                if(fileName.endsWith( ".class" )){
                    String classPath=path+"."+fileName.replaceAll( "\\.class","" );
                    this.classList.add( classPath );
                    System.out.println("classPath:"+classPath);
                }
            }
        }
    }
View Code

執行效果:

三、總結

  這裡總結了幾種將我們自定類交給Spring管理的方式,分別是:一、通過@Component註解,讓Spring自動掃描完成配置管理自動裝配;二、通過@Import註解,加入包路徑,讓Spring掃描,然後進行自動裝配;三、通過,建立配置類,通過@Configuration和@Bean完成類的載入和自動裝配。不管是哪一種方式,我們都可以完成自定義類由Spring容器去管理,理解了這種方式之後,我們再也不需要,使用類的時候new物件了,需要哪些物件,只需要從Spring容器中獲取便可。

原始碼地址:https://files-cdn.cnblogs.com/files/10158wsj/SpringbootDemo.zip

&n