聊聊Spring核心

ITPUB社群發表於2023-02-07


你好,我是yes。

猶記我當年初學 Spring 時,還需寫一個個 XML 檔案,當時心裡不知所以然,跟著網上的步驟一個一個配置下來,配錯一個看著 error 懵半天,不知所謂地瞎改到最後能跑就行,暗自感嘆 tmd 這玩意真複雜。

到後來用上 SpringBoot,看起來少了很多 XML 配置,心裡暗暗高興。起初根據預設配置跑的很正常,後面到需要改動的時候,我都不知道從哪下手。

稀裡糊塗地在大部分時候也能用,但是遇到奇怪點的問題都得找老李幫忙解決。

到後面發現還有 SpringCloud ,微服務的時代來臨了,我想不能再這般“猶抱琵琶半遮面”地使用 Spring 全家桶了。

一時間就鑽入各種 SpringCloud 細節原始碼中,希望能領悟框架真諦,最終無功而返且黯然傷神,再次感嘆 tmd 這玩意真複雜。

聊聊Spring核心

其間我已經意識到了是對 Spring 基礎框架的不熟悉,導致很多封裝點都不理解。

畢竟 SpringCloud 是基於 SpringBoot,而 SpringBoot 是基於 Spring。

於是乎我又回頭重學 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;
    }
}

看起來好像解決問題了,其實不然。

當專案比較小時,例如大學的大作業,上面這個操作其實問題不大,但是一到企業級應用上來說就複雜了。

因為涉及的邏輯多,封裝的服務類也多,之間的依賴也複雜,程式碼中可能要有ServiceB1ServiceB2...ServiceB100,而且相互之間還可能有依賴關係。

拋開依賴不說,就拿 ServiceB單純的單例邏輯程式碼,重複的邏輯可能需要寫成百上千份

擴充套件不易,以前可能 ServiceB 的操作都不需要事務,後面要上事務了,因此需要改 ServiceB 的程式碼,嵌入事務相關邏輯。

沒過多久 ServiceC 也要事務,一模一樣關於事務的程式碼又得在 ServiceC 上重複一遍,還有D、E、F...

對幾個 Service 事務要求又不一樣,還有巢狀事務的問題,總之有點麻煩。

忙了一段時間滿足事務需求,上線了,想著終於脫離了重複程式碼的噩夢可以好好休息一波。

緊接著又來了個需求,因為經常要排查線上問題,所以介面入參要打個日誌,方便問題排查,又得大刀闊斧操作一波全部改一遍。

有需求要改動很正常,但是每次改動需要做一大堆重複性工作,又累又沒技術含量還容易漏,這就不夠優雅了。

所以有人就開始想辦法,想從這個耦合泥沼中脫離出來。

拔高和剝離

人類絕大部分的發明都是因為懶,人們討厭重複的工作,而計算機最喜歡也最適合做重複的工作

既然之前的開發會有很多重複的工作,那為什麼不製造一個“東西”出來幫我們做這類重複的事情呢

就像以前人們手工一步一步組裝製造產品,每天一樣的步驟可能要重複上萬次,到後面人們研究出全自動機器來幫我們製造產品,解放了人們的雙手還提高了生產效率。

聊聊Spring核心

拔高了這個思想後,編碼的邏輯就從我們程式設計師想著且寫著 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 裡關於資料庫的配置:

聊聊Spring核心

可以看到我們的圖紙寫的很清楚,建立 mybatisMapperScannerConfigurer需要告訴它兩個屬性的值,比如第一個是sqlSessionFactoryBeanName,值是 sqlSessionFactory

sqlSessionFactory又依賴 dataSource,而  dataSource 又需要配置好 driverClassNameurl 等等。

所以,其實我們心裡很清楚一個產品(Bean)要建立的話具體需要什麼東西,只過不這個建立過程由 Spring 代勞了,我們只需要清楚的告訴它即可。

因此,不是說用了 Spring 我們不再關心 ServiceA 具體依賴怎樣的 ServiceBServiceB具體是如何建立成功的,而是說這些物件組裝的過程由 Spring 幫我們做好。

我們還是需要清楚地知道物件是如何建立的,因為我們需要畫好正確的圖紙告訴 Spring。

所以 Spring 其實就是一臺機器,根據我們給它的圖紙,自動幫我們建立關聯物件供我們使用,我們不需要顯示得在程式碼中寫好完整的建立程式碼

這些由 Spring 建立的物件例項,叫作 Bean。

我們如果要使用這些 Bean 可以從 Spring 中拿,Spring 將這些建立好的單例 Bean 放在一個 Map 中,透過名字或者型別我們可以獲取這些 Bean。

這就是 IOC

也正因為這些 Bean 都需要經過 Spring 這臺機器建立,不再是懶散地在程式碼的各個角落建立,我們就能很方便的基於這個統一收口做很多事情。

聊聊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 客戶端的選擇,預設會匯入 lettucejedis 兩個客戶端配置

聊聊Spring核心基於配置的先後順序會優先匯入 lettuce,然後再匯入 jedis。

如果掃描發現有 lettuce 那麼就用 lettuce 的 RedisConnectionFactory,而後面再載入 jedis 時,會基於@ConditionalOnMissingBean(RedisConnectionFactory.class) 來保證 jedis不會被注入,反之就會被注入。

聊聊Spring核心

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 暴露出很多擴充套件點供我們定製,如 BeanFactoryPostProcessorBeanPostProcessor,我們只需要實現這個介面就可以進行一些定製化的操作。

Spring 得到 Bean 的資訊後會根據反射來建立 Bean 例項,組裝 Bean 之間的依賴關係,其中就會穿插進原生的或我們定義的相關PostProcessor來改造Bean,替換一些屬性或代理原先的 Bean 邏輯。

最終建立完所有配置要求的Bean,將單例的 Bean 儲存在 map 中,提供 BeanFactory 供我們獲取使用 Bean。

使得我們編碼過程無需再關注 Bean 具體是如何建立的,也節省了很多重複性地編碼動作,這些都由我們建立的機器——Spring幫我們代勞。

大概就說這麼多了,我自己讀了幾遍也不知道到底有沒有把我想表達的東西說明白,其實我本來從原始碼層面來聊這個核心的,但是怕更難說清。

最後關於 Spring 的 IOC和 DI 概念的面試回答我之前也寫過了,可以看下,網址是:%E7%B2%BE%E9%80%89%E9%9D%A2%E8%AF%95%E9%A2%98%E8%A7%A3

聊聊Spring核心

我是yes,從一點點到億點點,我們下篇見~



來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2934192/,如需轉載,請註明出處,否則將追究法律責任。

相關文章