SpringBoot自動(dòng)裝配原理解析
什么是Spring Boot自動(dòng)裝配
Spring Boot自動(dòng)裝配是指在Spring Boot應(yīng)用啟動(dòng)時(shí),根據(jù)類路徑下的jar包依賴、Bean定義、各種配置文件等信息,自動(dòng)配置Spring應(yīng)用上下文的Bean。
這種機(jī)制極大地簡(jiǎn)化了配置工作,使得開發(fā)者可以更加專注于業(yè)務(wù)邏輯的實(shí)現(xiàn)。
在深入自動(dòng)裝配原理前,我們先看下 SPI 機(jī)制
。
SPI 機(jī)制
SPI(Service Provider Interface)是一種動(dòng)態(tài)替換服務(wù)提供者的機(jī)制。它允許一個(gè)服務(wù)接口有多個(gè)服務(wù)提供者,并且在程序運(yùn)行時(shí)動(dòng)態(tài)選擇一個(gè)服務(wù)提供者。
SPI又分為 JDK SPI
和 Spring SPI
。
JDK SPI
在 Java 平臺(tái)上,SPI 通常是通過 java.util.ServiceLoader
類實(shí)現(xiàn)的,這種機(jī)制在Java標(biāo)準(zhǔn)庫中廣泛應(yīng)用,如JDBC驅(qū)動(dòng)的管理。
SPI可以很靈活的讓接口和實(shí)現(xiàn)分離,讓服務(wù)提供者只提供接口,第三方來實(shí)現(xiàn),然后可以使用配置文件的方式來實(shí)現(xiàn)替換或者擴(kuò)展。
工作原理
Java SPI 的工作原理基于以下幾個(gè)步驟:
- 定義服務(wù)接口:首先定義一個(gè)服務(wù)接口(或抽象類),作為服務(wù)的規(guī)范。
- 提供服務(wù)實(shí)現(xiàn):編寫接口的具體實(shí)現(xiàn)類。
- 注冊(cè)服務(wù)實(shí)現(xiàn):在
META-INF/services
目錄下創(chuàng)建一個(gè)以接口全限定名為名的文件
,文件內(nèi)容為接口實(shí)現(xiàn)類的全限定名。 - 加載服務(wù)實(shí)現(xiàn):使用
java.util.ServiceLoader
來加載并使用這些實(shí)現(xiàn)。
舉例說明一下:
(1)創(chuàng)建一個(gè) DemoDAO
的接口
public interface DemoDao {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->}
(2)分別創(chuàng)建兩個(gè)實(shí)現(xiàn)類
public class MysqlDao implements DemoDao {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->}
public class OracleDao implements DemoDao {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->}
(3)在resources
下新建META-INF/services/
目錄,在該目錄下新建接口全限定名的文件com.study.spring.z_spi.DemoDao
,文件內(nèi)容是上面那兩個(gè)實(shí)現(xiàn)類。
com.study.spring.z_spi.MysqlDao com.study.spring.z_spi.OracleDao
(4)使用 JDK 提供的ServiceLoader
來測(cè)試下
public class JdkSpiApplication { public static void main(String[] args) { ServiceLoader<DemoDao> demoDaos = ServiceLoader.load(DemoDao.class); demoDaos.iterator().forEachRemaining(t -> { System.out.println(t); }); } }
輸出結(jié)果 :
com.study.spring.z_spi.MysqlDao@6e8cf4c6
com.study.spring.z_spi.OracleDao@12edcd21
JDBC DriverManager
Java SPI 機(jī)制在JDBC驅(qū)動(dòng)管理中的應(yīng)用主要體現(xiàn)在JDBC 4.0及以上版本的驅(qū)動(dòng)自動(dòng)發(fā)現(xiàn)和加載上。
在JDBC4.0之前,連接數(shù)據(jù)庫的時(shí)候,通常會(huì)用Class.forName(“com.mysql.jdbc.Driver”)先加載數(shù)據(jù)庫相關(guān)的驅(qū)動(dòng),然后再進(jìn)行獲取連接等的操作。
而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)來加載驅(qū)動(dòng),直接獲取連接就可以了,這種方式就是使用了Java的SPI擴(kuò)展機(jī)制來實(shí)現(xiàn)。
定義服務(wù)接口
在JDBC中,服務(wù)接口是由Java平臺(tái)定義的java.sql.Driver
接口,所有的JDBC驅(qū)動(dòng)都必須實(shí)現(xiàn)這個(gè)接口,以提供與數(shù)據(jù)庫建立連接的能力。
提供服務(wù)實(shí)現(xiàn)
數(shù)據(jù)庫廠商(如MySQL、Oracle等)為它們的數(shù)據(jù)庫提供JDBC驅(qū)動(dòng)程序,這些驅(qū)動(dòng)程序?qū)崿F(xiàn)了java.sql.Driver
接口。
注冊(cè)服務(wù)提供者
JDBC驅(qū)動(dòng)的注冊(cè)是通過在驅(qū)動(dòng)程序的JAR包中的META-INF/services
目錄下創(chuàng)建一個(gè)名為java.sql.Driver
的文件來完成的。這個(gè)文件包含了實(shí)現(xiàn)java.sql.Driver
接口的驅(qū)動(dòng)類的全限定名。當(dāng)JVM啟動(dòng)時(shí),它會(huì)查找這個(gè)文件,并加載其中指定的驅(qū)動(dòng)類。
加載服務(wù)提供者
在JDBC 4.0及以上版本中,DriverManager
類在初始化時(shí)會(huì)使用Java的SPI機(jī)制自動(dòng)加載所有在META-INF/services/java.sql.Driver
文件中指定的驅(qū)動(dòng)類。
這是通過ServiceLoader類實(shí)現(xiàn)的,它會(huì)查找并加載所有可用的JDBC驅(qū)動(dòng)實(shí)現(xiàn)。
連接管理
當(dāng)應(yīng)用程序嘗試通過DriverManager.getConnection()
方法連接數(shù)據(jù)庫時(shí),DriverManager會(huì)遍歷所有已加載的驅(qū)動(dòng)實(shí)例,嘗試建立連接。一旦某個(gè)驅(qū)動(dòng)成功建立連接,它就會(huì)返回這個(gè)連接,并且不會(huì)繼續(xù)嘗試其他的驅(qū)動(dòng)實(shí)例。
SpringBoot SPI 機(jī)制
Spring Boot 對(duì) SPI 機(jī)制進(jìn)行了擴(kuò)展,以支持其自動(dòng)配置和模塊化架構(gòu)。
Spring Boot 利用 spring.factories
(從 SpringBoot 2.7 起自動(dòng)配置不推薦使用 /META-INF/spring.factories
文件,而是在/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
)文件,這個(gè)文件列出了與自動(dòng)配置相關(guān)的接口及其實(shí)現(xiàn)類,Spring Boot 啟動(dòng)時(shí)會(huì)加載這些配置。
spring.factories
這個(gè)文件里面使用鍵值對(duì)的格式列出了多種服務(wù)類型及其對(duì)應(yīng)的實(shí)現(xiàn)類,常見的服務(wù)類型包括:
- org.springframework.boot.autoconfigure.EnableAutoConfiguration:用于自動(dòng)配置。
- org.springframework.context.ApplicationListener:用于應(yīng)用事件監(jiān)聽器。
- org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider:用于模板引擎的可用性判斷。
# Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.autoconfigure.BackgroundPreinitializer # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
不使用SPI
現(xiàn)在我們先來看下,不使用 SPI 機(jī)制,怎么實(shí)現(xiàn) bean 的配置。
新建一個(gè)項(xiàng)目,將新類MyAppDemo
注入 IOC 容器。
在另一個(gè)項(xiàng)目 demo 中,引入 myApp 的依賴,并測(cè)試調(diào)用 test 方法。
@SpringBootApplication public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } } @SpringBootTest class StarterDemoApplicationTests { @Autowired private ApplicationContext applicationContext; @Test void contextLoads() { MyAppDemo demo = applicationContext.getBean(MyAppDemo.class); demo.test(); } }
測(cè)試發(fā)現(xiàn)找不到該 bean 對(duì)象
為什么引入的第三方依賴包中的 bean 沒有生效呢?
- 原因是因?yàn)椋陬惿咸砑?code>@Component注解來聲明bean對(duì)象時(shí),還需要保證
@Component
注解能被Spring的組件掃描到。 - SpringBoot項(xiàng)目中的
@SpringBootApplication
注解,具有包掃描的作用,但是它只會(huì)掃描啟動(dòng)類所在的當(dāng)前包以及子包。 - 當(dāng)前包:com.starter.demo, 第三方依賴中提供的包:com.myapp.demo(掃描不到)
所以,有兩種方案可以解決
- @ComponentScan 組件掃描第三方依賴的包路徑;
- @Import 導(dǎo)入(使用@Import導(dǎo)入的類會(huì)被Spring加載到IOC容器中)。
@ComponentScan 組件掃描
@SpringBootApplication @ComponentScan(basePackages = {"com.starter","com.myapp"}) public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } }
缺點(diǎn):當(dāng)需要引入大量的第三方依賴,就需要在啟動(dòng)類上配置大量要掃描的包,這種方式會(huì)很繁瑣。
Import 導(dǎo)入
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Import { /** * {@link Configuration @Configuration}, {@link ImportSelector}, * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. */ Class<?>[] value(); }
從源碼可以看到,導(dǎo)入形式有以下幾種
- 普通類
- 配置類
- ImportSelector的實(shí)現(xiàn)類
- ImportBeanDefinitionRegistrar的實(shí)現(xiàn)類
導(dǎo)入普通類
@SpringBootApplication @Import(MyAppDemo.class) public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } }
導(dǎo)入配置類
去掉MyAppDemo
類上的@Component注解,新建一個(gè)MyAppConfig
配置類。
@Configuration public class MyAppConfig { @Bean public MyAppDemo myAppDemo() { return new MyAppDemo(); } }
在啟動(dòng)類上導(dǎo)入配置類
@SpringBootApplication @Import(MyAppConfig.class) public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } }
導(dǎo)入ImportSelector的實(shí)現(xiàn)類
新建 ImportSelector
的實(shí)現(xiàn)類
public class MyAppImportSelector implements ImportSelector { public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] {"com.myapp.demo.MyAppDemo"}; } }
在啟動(dòng)類上導(dǎo)入MyAppImportSelector
@SpringBootApplication @Import(MyAppImportSelector.class) public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } }
導(dǎo)入ImportBeanDefinitionRegistrar的實(shí)現(xiàn)類
新建 ImportBeanDefinitionRegistrar
的實(shí)現(xiàn)類
public class MyAppImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(MyAppDemo.class); registry.registerBeanDefinition("myAppDemoTest", builder.getBeanDefinition()); } }
在啟動(dòng)類上導(dǎo)入MyAppImportBeanDefinitionRegistrar
@SpringBootApplication @Import(MyAppImportBeanDefinitionRegistrar.class) public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } }
模塊裝配
通過@Import
注解,我們可以導(dǎo)入第三方依賴包中的配置類。
但是基于上面的方式,我們?cè)谝氲谌揭蕾嚂r(shí),還要知道第三方依賴中有哪些配置類和哪些Bean對(duì)象?相當(dāng)麻煩!
而第三方依賴自己最清楚自己有哪些配置類、有那些 Bean 對(duì)象,它提供一個(gè)注解,通過這個(gè)注解,外部系統(tǒng)可以引入自己所需要的 Bean 對(duì)象。
這個(gè)注解一般都以@EnableXxx
開頭,注解中封裝的就是@Import
注解,外部系統(tǒng)在使用時(shí)只需要加上@EnableXxxxx
注解即可。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(MyAppConfig.class) public @interface EnableMyApp {}
@SpringBootApplication @EnableMyApp public class StarterDemoApplication { public static void main(String[] args) { SpringApplication.run(StarterDemoApplication.class, args); } }
spring 中的模塊裝配,是在3.1 之后引入了大量的@EnableXXX 注解,來快速整合激活相對(duì)應(yīng)的模塊。
如:
● EnableTransactionManagement :開啟注解事務(wù)驅(qū)動(dòng)
● EnableWebMvc :激活 SpringWebMvc
● EnableAspectJAutoProxy :開啟注解 AOP 編程
● EnableScheduling :開啟調(diào)度功能(定時(shí)任務(wù))
模塊裝配的核心原則:自定義注解+@Import 導(dǎo)入組件
使用SPI
即使是采用@EnableXXX
注解,還是覺得麻煩怎么辦?
我想引入第三方依賴后,直接就去使用它,而不是再單獨(dú)寫一個(gè)什么什么注解。
下面我們來看看基于 Spring 的 SPI 機(jī)制怎么去實(shí)現(xiàn)。
在在resources
目錄下創(chuàng)建META-INF 目錄,并新建 spring.factories
文件,文件內(nèi)容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.myapp.demo.MyAppConfig
@Configuration public class MyAppConfig { @Bean public MyAppDemo myAppDemo() { return new MyAppDemo(); } }
我們只是引入了第三方依賴包,并沒有手動(dòng)配置,也沒有寫什么注解啊,就可以通過IOC容器或DI依賴拿到bean對(duì)象了,這就是SpringBoot自動(dòng)配置的強(qiáng)大之處。
自動(dòng)裝配源碼
從 SpringBoot 核心注解說起
@SpringBootConfiguration @EnableAutoConfiguration @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) public @interface SpringBootApplication {}
重點(diǎn)看@EnableAutoConfiguration
,核心中的核心,重點(diǎn)中的重點(diǎn)。
點(diǎn)進(jìn)去 AutoConfigurationImportSelector
可以看到 AutoConfigurationImportSelector
實(shí)現(xiàn)了 DeferredImportSelector
,而DeferredImportSelector
又繼承了ImportSelector
。
AutoConfigurationImportSelector
類中重寫了ImportSelector
接口的selectImports()
方法:
再點(diǎn)進(jìn)去
可以看到這個(gè)getCandidateConfigurations()
方法,就是去獲取META-INF/spring.factories
文件中配置類的集合。
再接著點(diǎn)點(diǎn)點(diǎn)
以上就是SpringBoot自動(dòng)裝配原理解析的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot自動(dòng)裝配的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Nacos后臺(tái)頻繁打印get changedGroupKeys:[]的問題及解決
這篇文章主要介紹了Nacos后臺(tái)頻繁打印get changedGroupKeys:[]的問題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01詳解IntelliJ IDEA 中如何配置多個(gè)jdk版本即(1.7和1.8兩個(gè)jdk都可用)
這篇文章主要介紹了詳解IntelliJ IDEA 中如何配置多個(gè)jdk版本即(1.7和1.8兩個(gè)jdk都可用),非常具有實(shí)用價(jià)值,需要的朋友可以參考下2017-11-11java數(shù)據(jù)結(jié)構(gòu)基礎(chǔ):單,雙向鏈表
這篇文章主要介紹了Java的數(shù)據(jù)解構(gòu)基礎(chǔ),希望對(duì)廣大的程序愛好者有所幫助,同時(shí)祝大家有一個(gè)好成績,需要的朋友可以參考下,希望能給你帶來幫助2021-07-07Mybatis一對(duì)多查詢的兩種姿勢(shì)(值得收藏)
這篇文章主要給大家介紹了關(guān)于Mybatis一對(duì)多查詢的兩種姿勢(shì),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05SpringBoot+WebSocket實(shí)現(xiàn)IM及時(shí)通訊的代碼示例
項(xiàng)目中碰到需要及時(shí)通訊的場(chǎng)景,使用springboot集成websocket,即可實(shí)現(xiàn)簡(jiǎn)單的及時(shí)通訊,本文介紹springboot如何集成websocket、IM及時(shí)通訊需要哪些模塊、開發(fā)和部署過程中遇到的問題、以及實(shí)現(xiàn)小型IM及時(shí)通訊的代碼,需要的朋友可以參考下2023-10-10Java實(shí)現(xiàn)文件壓縮與解壓的示例[zip格式,gzip格式]
本篇文章主要介紹了Java實(shí)現(xiàn)文件壓縮與解壓的示例[zip格式,gzip格式],具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下。2017-01-01