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






