猶記我當(dāng)年初學(xué) Spring 時(shí),還需寫一個(gè)個(gè) XML 文件,當(dāng)時(shí)心里不知所以然,跟著網(wǎng)上的步驟一個(gè)一個(gè)配置下來,配錯(cuò)一個(gè)看著 error 懵半天,不知所謂地瞎改到最后能跑就行,暗自感嘆 tmd 這玩意真復(fù)雜。
到后來用上 SpringBoot,看起來少了很多 XML 配置,心里暗暗高興。起初根據(jù)默認(rèn)配置跑的很正常,后面到需要改動(dòng)的時(shí)候,我都不知道從哪下手。
稀里糊涂地在大部分時(shí)候也能用,但是遇到奇怪點(diǎn)的問題都得找老李幫忙解決。
到后面發(fā)現(xiàn)還有 SpringCloud ,微服務(wù)的時(shí)代來臨了,我想不能再這般“猶抱琵琶半遮面”地使用 Spring 全家桶了。
一時(shí)間就鉆入各種 SpringCloud 細(xì)節(jié)源碼中,希望能領(lǐng)悟框架真諦,最終無功而返且黯然傷神,再次感嘆 tmd 這玩意真復(fù)雜。
其間我已經(jīng)意識(shí)到了是對(duì) Spring 基礎(chǔ)框架的不熟悉,導(dǎo)致很多封裝點(diǎn)都不理解。
畢竟 SpringCloud 是基于 SpringBoot,而 SpringBoot 是基于 Spring。
于是乎我又回頭重學(xué) Spring,不再一來就是扎入各種細(xì)節(jié)中,我換了個(gè)策略,先從高緯角度總覽 Spring ,理解核心原理后再攻克各種分支脈路。
于是我,我變強(qiáng)了。
其實(shí)學(xué)任何東西都是一樣,先要總覽全貌再深入其中,等回過頭之后再進(jìn)行總結(jié)。
這篇我打算用自己的理解來闡述下 Spring 的核心(思想),礙于個(gè)人表達(dá)能力可能有不對(duì)或啰嗦的地方,還請(qǐng)擔(dān)待,如有錯(cuò)誤懇請(qǐng)指出。
拋開IOC、DI去想為什么要有Spring
在初學(xué) JAVA 時(shí),我們理所當(dāng)然得會(huì)寫出這樣的代碼:
public class ServiceA {
private ServiceB serviceB = new ServiceB();
}
我們把一些邏輯封裝到 ServiceB 中,當(dāng) ServiceA 需用到這些邏輯時(shí)候,在 ServiceA 內(nèi)部 new 個(gè)ServiceB 。
如果 ServiceB 封裝的邏輯非常通用,還會(huì)有 ServiceC.....ServiceF等都需要依賴它,也就是說代碼里面各個(gè)地方都需要 new 個(gè)ServiceB ,這樣一來如果它的構(gòu)造方法發(fā)生變化,你就要在所有用到它的地方進(jìn)行代碼修改。
比如 ServiceB 實(shí)例的創(chuàng)建需要 ServiceC ,代碼就改成這樣:
public class ServiceA {
private ServiceB serviceB = new ServiceB(new ServiceC());
}
確實(shí)有這個(gè)問題。
但實(shí)際上如若我們封裝通用的service 邏輯,沒必要每次都 new 個(gè)實(shí)例,也就是說單例就夠了,我們的系統(tǒng)只需要 new一個(gè) ServiceB 供各個(gè)對(duì)象使用,就能解決這個(gè)問題。
public class ServiceA {
private ServiceB serviceB = ServiceB.getInstance();
}
public class ServiceB {
private static ServiceB instance = new ServiceB(new ServiceC());
private ServiceB(){}
public static ServiceB getInstance(){
return instance;
}
}
看起來好像解決問題了,其實(shí)不然。
當(dāng)項(xiàng)目比較小時(shí),例如大學(xué)的大作業(yè),上面這個(gè)操作其實(shí)問題不大,但是一到企業(yè)級(jí)應(yīng)用上來說就復(fù)雜了。
因?yàn)樯婕暗倪壿嫸啵庋b的服務(wù)類也多,之間的依賴也復(fù)雜,代碼中可能要有ServiceB1、ServiceB2...ServiceB100,而且相互之間還可能有依賴關(guān)系。
拋開依賴不說,就拿 ServiceB單純的單例邏輯代碼,重復(fù)的邏輯可能需要寫成百上千份。
且擴(kuò)展不易,以前可能 ServiceB 的操作都不需要事務(wù),后面要上事務(wù)了,因此需要改 ServiceB 的代碼,嵌入事務(wù)相關(guān)邏輯。
沒過多久 ServiceC 也要事務(wù),一模一樣關(guān)于事務(wù)的代碼又得在 ServiceC 上重復(fù)一遍,還有D、E、F...
對(duì)幾個(gè) Service 事務(wù)要求又不一樣,還有嵌套事務(wù)的問題,總之有點(diǎn)麻煩。
忙了一段時(shí)間滿足事務(wù)需求,上線了,想著終于脫離了重復(fù)代碼的噩夢(mèng)可以好好休息一波。
緊接著又來了個(gè)需求,因?yàn)榻?jīng)常要排查線上問題,所以接口入?yún)⒁騻€(gè)日志,方便問題排查,又得大刀闊斧操作一波全部改一遍。
有需求要改動(dòng)很正常,但是每次改動(dòng)需要做一大堆重復(fù)性工作,又累又沒技術(shù)含量還容易漏,這就不夠優(yōu)雅了。
所以有人就開始想辦法,想從這個(gè)耦合泥沼中脫離出來。
拔高和剝離
人類絕大部分的發(fā)明都是因?yàn)閼校藗冇憛捴貜?fù)的工作,而計(jì)算機(jī)最喜歡也最適合做重復(fù)的工作。
既然之前的開發(fā)會(huì)有很多重復(fù)的工作,那為什么不制造一個(gè)“東西”出來幫我們做這類重復(fù)的事情呢?
就像以前人們手工一步一步組裝制造產(chǎn)品,每天一樣的步驟可能要重復(fù)上萬次,到后面人們研究出全自動(dòng)機(jī)器來幫我們制造產(chǎn)品,解放了人們的雙手還提高了生產(chǎn)效率。
拔高了這個(gè)思想后,編碼的邏輯就從我們程序員想著且寫著 ServiceA 依賴具體的 ServiceB ,且一個(gè)字母一個(gè)字母的敲完 ServiceB 具體是如何實(shí)例化的代碼,變成我們只關(guān)心 ServiceA 依賴 ServiceB,但 ServiceB 是如何生成的我們不管,由那個(gè)“東西”幫我們生成它且關(guān)聯(lián)好 ServiceA 和 ServiceB。
public class ServiceA {
@注入
private ServiceB serviceB;
}
聽起來好像有點(diǎn)懸乎,其實(shí)不然。
還是拿機(jī)器說事,我們創(chuàng)造這臺(tái)機(jī)器,如果要生產(chǎn)產(chǎn)品 A,我們只要畫好圖紙 A,將圖紙 A 塞到這個(gè)機(jī)器里,機(jī)器識(shí)別圖紙 A,按照我們圖紙 A 的設(shè)計(jì)制造出我們要的產(chǎn)品 A。
Spring就是這臺(tái)機(jī)器,圖紙就是依托 Spring 管理的對(duì)象代碼以及那些 XML 文件(或標(biāo)注了@Configuration的類)。
這時(shí)候邏輯就轉(zhuǎn)變了。程序員知道 ServiceA 具體依賴哪個(gè) ServiceB,但是我們不需要顯示的在代碼中寫上完整的關(guān)于如何創(chuàng)建 ServiceB 的邏輯,我們只需要寫好配置文件,具體地創(chuàng)建和關(guān)聯(lián)由 Spring 幫我們做。
繼續(xù)拿機(jī)器舉例,我們給了圖紙(配置),機(jī)器幫我們制造產(chǎn)品,具體如何制造出來不需要我們操心,但是我們心里是有數(shù)的,因?yàn)槲覀兊膱D紙寫明了制造 ServiceA 需要哪樣的 ServiceB,而那樣的 ServiceB 又需要哪樣的 ServiceC等等邏輯。
我找個(gè)圖紙例子,Spring 里關(guān)于數(shù)據(jù)庫(kù)的配置:
可以看到我們的圖紙寫的很清楚,創(chuàng)建 MyBatis 的MApperScannerConfigurer需要告訴它兩個(gè)屬性的值,比如第一個(gè)是sqlSessionFactoryBeanName,值是 sqlSessionFactory。
而sqlSessionFactory又依賴 dataSource,而 dataSource 又需要配置好 driverClassName、url 等等。
所以,其實(shí)我們心里很清楚一個(gè)產(chǎn)品(Bean)要?jiǎng)?chuàng)建的話具體需要什么東西,只過不這個(gè)創(chuàng)建過程由 Spring 代勞了,我們只需要清楚的告訴它即可。
因此,不是說用了 Spring 我們不再關(guān)心 ServiceA 具體依賴怎樣的 ServiceB、ServiceB具體是如何創(chuàng)建成功的,而是說這些對(duì)象組裝的過程由 Spring 幫我們做好。
我們還是需要清楚地知道對(duì)象是如何創(chuàng)建的,因?yàn)槲覀冃枰嫼谜_的圖紙告訴 Spring。
所以 Spring 其實(shí)就是一臺(tái)機(jī)器,根據(jù)我們給它的圖紙,自動(dòng)幫我們創(chuàng)建關(guān)聯(lián)對(duì)象供我們使用,我們不需要顯示得在代碼中寫好完整的創(chuàng)建代碼。
這些由 Spring 創(chuàng)建的對(duì)象實(shí)例,叫作 Bean。
我們?nèi)绻褂眠@些 Bean 可以從 Spring 中拿,Spring 將這些創(chuàng)建好的單例 Bean 放在一個(gè) Map 中,通過名字或者類型我們可以獲取這些 Bean。
這就是 IOC。
也正因?yàn)檫@些 Bean 都需要經(jīng)過 Spring 這臺(tái)機(jī)器創(chuàng)建,不再是懶散地在代碼的各個(gè)角落創(chuàng)建,我們就能很方便的基于這個(gè)統(tǒng)一收口做很多事情。
比如當(dāng)我們的 ServiceB 標(biāo)注了 @Transactional 注解,由 Spring 解析到這個(gè)注解就能明白這個(gè) ServiceB 是需要事務(wù)的,于是乎織入的事務(wù)的開啟、提交、回滾等操作。
但凡標(biāo)記了 @Transactional 注解的都自動(dòng)添加事務(wù)邏輯,這對(duì)我們而言減輕了太多重復(fù)的代碼,只要在需要事務(wù)的方法或類上添加 @Transactional注解即可由 Spring 幫我們補(bǔ)充上事務(wù)功能,重復(fù)的操作都由 Spring 完成。
再比如我們需要在所有的 controller 上記錄請(qǐng)求入?yún)ⅲ@也非常簡(jiǎn)單,我們只要寫個(gè)配置,告訴 Spring xxx路徑(controller包路徑)下的類的每個(gè)方法的入?yún)⒍夹枰涗浽?log 里,并且把日志打印邏輯代碼也寫上。
Spring 解析完這個(gè)配置后就得到了這個(gè)命令,于是乎在創(chuàng)建后面的 Bean 時(shí)就看看它所處的包是否符合上述的配置,若符合就把我們添加日志打印邏輯和原有的邏輯編織起來。
這樣就把重復(fù)的日志打印動(dòng)作操作抽象成一個(gè)配置,Spring 這臺(tái)機(jī)器識(shí)別配置后執(zhí)行我們的命令完成這些重復(fù)的動(dòng)作。
這就叫 AOP。
至此我相信你對(duì) Spring 的由來和核心概念有了一定的了解,基于上面的特性能做的東西有很多。
因?yàn)橛辛?Spring 這個(gè)機(jī)器統(tǒng)一收口處理,我們就可以靈活在不同時(shí)期提供很多擴(kuò)展點(diǎn),比如配置文件解析的時(shí)候、Bean初始化的前后,Bean實(shí)例化的前后等等。
基于這些擴(kuò)展點(diǎn)就能實(shí)現(xiàn)很多功能,例如 Bean 的選擇性加載、占位符的替換、代理類(事務(wù)等)的生成。
好比 SpringBoot redis 客戶端的選擇,默認(rèn)會(huì)導(dǎo)入 lettuce 和 jedis兩個(gè)客戶端配置
基于配置的先后順序會(huì)優(yōu)先導(dǎo)入 lettuce,然后再導(dǎo)入 jedis。
如果掃描發(fā)現(xiàn)有 lettuce 那么就用 lettuce 的 RedisConnectionFactory,而后面再加載 jedis 時(shí),會(huì)基于@ConditionalOnMissingBean(RedisConnectionFactory.class) 來保證 jedis不會(huì)被注入,反之就會(huì)被注入。
ps:@ConditionalOnMissingBean(xx.class) 如果當(dāng)前沒有xx.class才能生成被這個(gè)注解修飾的bean
就上面這個(gè)特性就是基于 Spring 提供的擴(kuò)展點(diǎn)來實(shí)現(xiàn)的。
很靈活地讓我們替換所需的 redis 客戶端,不用改任何使用的代碼,只需要改個(gè)依賴,比如要從默認(rèn)的 lettuce 變成 jedis,只需要改個(gè) maven 配置,去除 lettuce 依賴,引入 jedis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
說這么多其實(shí)就是想表達(dá):Spring 全家桶提供的這些擴(kuò)展和封裝可以靈活地滿足我們的諸多需求,而這些靈活都是基于 Spring 的核心 IOC 和 AOP 而來的。
最后
最后我用一段話來簡(jiǎn)單描述下 Spring 的原理:
Spring 根據(jù)我們提供的配置類和XML配置文件,解析其中的內(nèi)容,得到它需要管理的 Bean 的信息以及之間的關(guān)聯(lián),并且 Spring 暴露出很多擴(kuò)展點(diǎn)供我們定制,如 BeanFactoryPostProcessor、BeanPostProcessor,我們只需要實(shí)現(xiàn)這個(gè)接口就可以進(jìn)行一些定制化的操作。
Spring 得到 Bean 的信息后會(huì)根據(jù)反射來創(chuàng)建 Bean 實(shí)例,組裝 Bean 之間的依賴關(guān)系,其中就會(huì)穿插進(jìn)原生的或我們定義的相關(guān)PostProcessor來改造Bean,替換一些屬性或代理原先的 Bean 邏輯。
最終創(chuàng)建完所有配置要求的Bean,將單例的 Bean 存儲(chǔ)在 map 中,提供 BeanFactory 供我們獲取使用 Bean。
使得我們編碼過程無需再關(guān)注 Bean 具體是如何創(chuàng)建的,也節(jié)省了很多重復(fù)性地編碼動(dòng)作,這些都由我們創(chuàng)建的機(jī)器——Spring幫我們代勞。
大概就說這么多了,我自己讀了幾遍也不知道到底有沒有把我想表達(dá)的東西說明白,其實(shí)我本來從源碼層面來聊這個(gè)核心的,但是怕更難說清。