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