更多Spring文章,歡迎點選 一灰灰Blog-Spring專題
FactoryBean在Spring中算是一個比較有意思的存在了,雖然在日常的業務開發中,基本上不怎麼會用到,但在某些場景下,如果用得好,卻可以實現很多有意思的東西
本篇博文主要介紹如何通過FactoryBean來實現一個類SPI機制的微型應用框架
文章內涉及到的知識點
- SPI機制
- FactoryBean
- JDK動態代理
I. 相關知識點
在看下面的內容之前,得知道一下什麼是SPI,以及SPI的用處和JDK實現SPI的方式,對於這一塊有興趣瞭解的童鞋,可以看一下個人之前寫的相關文章
1. demo背景說明
在開始之前,有必要了解一下,我們準備做的這個東西,到底適用於什麼樣的場景。
在電商中,有一個比較恰當的例子,商品詳情頁的展示。拿淘寶系的詳情頁作為背景來說明(沒有在阿里工作過,下面的東西純粹是為了說明應用場景而展開)
假設有這麼三個詳情頁,我們設定一個大前提,底層的資料層提供方都是一套的,商品詳情展示的服務完全可以做到複用,即三個性情頁中,絕大多數的東西都一樣,只是不同的詳情頁車重點不同而已。
如上圖中,我們假定有細微區別的幾個地方
位置 | 淘寶詳情 | 天貓詳情 | 鹹魚詳情 | 說明 |
---|---|---|---|---|
banner | 顯示淘寶的背景牆 | 顯示天貓的廣告位 | 鹹魚的坑位 | 三者資料結構完全一致,僅圖片url不同 |
推薦 | 推薦同類商品 | 推薦店家其他商品 | 推薦同類二手產品 | 資料結構相同,內容不同 |
評價 | 商品評價 | 商品評價 | 沒有評價,改為留言 | |
促銷 | 優惠券 | 天貓積分券 | 沒有券 | - |
根據上面的簡單對比,其實只想表達一個意思,業務基本上一致,僅僅只有很少的一些東西不同,需要定製化,這個時候可以考慮用SPI來支援定製化的服務
2. SPI簡述
a. 基本定義
SPI的全名為Service Provider Interface,簡單的總結下java spi機制的思想。我們系統裡抽象的各個模組,往往有很多不同的實現方案,比如日誌模組的方案,xml解析模組、jdbc模組的方案等。面向的物件的設計裡,我們一般推薦模組之間基於介面程式設計,模組之間不對實現類進行硬編碼。一旦程式碼裡涉及具體的實現類,就違反了可拔插的原則,如果需要替換一種實現,就需要修改程式碼。為了實現在模組裝配的時候能不在程式裡動態指明,這就需要一種服務發現機制。 java spi就是提供這樣的一個機制:為某個介面尋找服務實現的機制
上面是相對正視一點的介紹,簡單一點,符合本文設計目標的介紹如下
- 介面方式引用
- 具體執行時,根據某些條件,選中實際的子類執行
通過上面的描述,可以發現一個最大的優點就是:
- 通過擴充套件介面的實現,就可以實現服務擴充套件;而不需要改原來的業務程式碼
b. demo輔助說明
一個簡單的應用場景如下
這個報警系統中,對於使用者而言,通過 IAlarm#sendMsg(level, msg)
來執行報警傳送的方式,然而這一行的具體執行者是(忽略,日誌報警,郵件報警還是簡訊報警)不確定的,通過SPI的實現方式將是如下
- 如果level為1,則忽略報警內容
- 如果level為2,則採用日誌報警的方式來報警
- ...
如果我們想新新增一種報警方式呢?那也很簡單,新建一個報警的實現
- level == 5, 則採用微信報警
然後對於使用者而言,其他的地方都不用改,只是在傳入的level引數換成5就可以了
3. 代理模式簡述
代理模式,在Spring中可以說是非常非常非常常見的一種設計模式了,大名鼎鼎的AOP就是這個實現的一個經典case,常見的代理有兩種實現方式
- JDK方式
- CGLIB方式
簡單說一下,代理模式的定義和說明如下,更多詳情可以參考: 實現MVC: 3. AOP實現準備篇代理模式
其實在現實生活中代理模式還是非常多得,這裡引入一個代理商的概念來加以描述,本來一個水果園直接賣水果就好了,現在中間來了一個水果超市,水果園的代銷商,對水果進行分類,包裝,然後再賣給使用者,這其實也算是一種代理
百科定義:為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個物件不適合或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用。
II. 方案設計與實現
瞭解完上面的前提之後,我們可以考慮下如何實現一個Spring容器中的SPI工具包
1. 目標拆分
首先確定大的生態環境為Spring,我們針對Bean做SPI功能的擴充套件,即定義一個SPI的介面,然後可以有多個實現類,並且全部都宣告為Bean;
SPI的一個重要特點就是可以選中不同的實現來執行具體的程式碼,那麼放在這裡,就會有兩種方案
- 方案一:依賴注入時,直接根據選擇條件,注入一個滿足的例項,後續所有的SPI呼叫,都將走這個具體的例項呼叫執行
- 方案二:依賴注入時,不注入具體的例項,反而註冊一個代理類,在代理類中,根據呼叫的引數來選擇具體匹配的例項來執行,因此後續的呼叫具體選中的例項將與傳入的引數有關
方案對比
方案一 | 方案二 |
---|---|
接近JDK的SPI使用方式 | 代理方式選中匹配的例項 |
優點:簡單,使用以及後續維護簡單 | 靈活, 支援更富想象力的擴充套件 |
缺點:一對一,複用性不夠,不能支援前面的case | 實現和呼叫方式跟繁瑣一點,需要傳入用於選擇具體例項條件引數 每次選擇子類都需要額外計算 |
對比上面的兩個方案之後,選中第二個(當然主要原因是為了演示FactoryBean和代理實現SPI機制,如果選擇方案一就沒有這兩個什麼事情了)
選中方案之後,目標拆分就比較清晰了
- 定義SPI介面,以及SPI的使用姿勢(前提)
- 一個生成代理類的FactoryBean (核心)
2. 方案設計
針對前面拆分的目標,進行方案設計,第一步就是介面相關的定義了
a. 介面定義
設計的SPI微型框架的核心為:在執行的時候,根據傳入的引數來決定具體的例項來執行,因此我們的介面設計中,至少有一個根據傳入的引數來判斷是否選中這個例項的介面
public interface ISpi<T> {
boolean verify(T condition);
}
複製程式碼
看到上面的實現之後,就會有一個疑問,如果有多個子類都滿足這個條件怎麼辦?因此可以加一個排序的介面,返回優先順序最高的匹配者
public interface ISpi<T> {
boolean verify(T condition);
/**
* 排序,數字越小,優先順序越高
* @return
*/
default int order() {
return 10;
}
}
複製程式碼
介面定義之後,使用者應該怎麼用呢?
b. 使用約束
spi實現的約束
基於JDK的代理模式,一個最大的前提就是,只能根據介面來生成代理類,因此在使用SPI的時候,我們希望使用者先定義一個介面來繼承ISpi
,然後具體的SPI實現這個介面即可
其次就是在Spring的生態下,要求所有的SPI實現都是Bean,需要自動掃描或者配置註解方式宣告,否者代理類就不太好獲取所有的SPI實現了
spi使用的約束
在使用SPI介面時,通過介面的方式來引入,因為我們實際注入的會是代理類,因此不要寫具體的實現類
單獨看上面的說明,可能不太好理解,建議結合下面的例項演示對比
c. 代理類生成
這個屬於最核心的地方了(雖說重要性為No1,但實現其實非常非常簡單)
代理類主要目的就是在具體呼叫執行時,根據傳入的引數來選中具體的執行者,執行後並返回對應的結果
- 獲取所有的SPI實現類(
org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(java.lang.Class<T>)
) - 通過jdk生成代理類,代理類中,遍歷所有的SPI實現,根據傳入的第一個引數作為條件進行匹配,找出首個命中的SPI實現類,執行
將上面的步驟具體實現,也就比較簡單了
public class SpiFactoryBean<T> implements FactoryBean<T> {
private Class<? extends ISpi> spiClz;
private List<ISpi> list;
public SpiFactoryBean(ApplicationContext applicationContext, Class<? extends ISpi> clz) {
this.spiClz = clz;
Map<String, ? extends ISpi> map = applicationContext.getBeansOfType(spiClz);
list = new ArrayList<>(map.values());
list.sort(Comparator.comparingInt(ISpi::order));
}
@Override
@SuppressWarnings("unchecked")
public T getObject() throws Exception {
// jdk動態代理類生成
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
for (ISpi spi : list) {
if (spi.verify(args[0])) {
// 第一個引數作為條件選擇
return method.invoke(spi, args);
}
}
throw new NoSpiChooseException("no spi server can execute! spiList: " + list);
}
};
return (T) Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{spiClz},
invocationHandler);
}
@Override
public Class<?> getObjectType() {
return spiClz;
}
}
複製程式碼
3. 例項演示
話說方案設計之後,應該就是實現了,然而因為實現過於簡單,設計的過程中,也就順手寫了,就是上面的一個介面定義 ISpi
和一個用來生成動態代理類的SpiFactoryBean
接下來寫一個簡單的例項用於功能演示,定義一個IPrint
用於文字輸出,並給兩個實現,一個控制檯輸出,一個日誌輸出
public interface IPrint extends ISpi<Integer> {
default void execute(Integer level, Object... msg) {
print(msg.length > 0 ? (String) msg[0] : null);
}
void print(String msg);
}
複製程式碼
具體的實現類如下,外部使用者通過execute
方法實現呼叫,其中level<=0
時選擇控制檯輸出;否則選則日誌檔案方式輸出
@Component
public class ConsolePrint implements IPrint {
@Override
public void print(String msg) {
System.out.println("console print: " + msg);
}
@Override
public boolean verify(Integer condition) {
return condition <= 0;
}
}
@Slf4j
@Component
public class LogPrint implements IPrint {
@Override
public void print(String msg) {
log.info("log print: {}", msg);
}
@Override
public boolean verify(Integer condition) {
return condition > 0;
}
}
複製程式碼
前面的步驟和一般的寫法沒有什麼區別,使用的姿勢又是怎樣的呢?
@SpringBootApplication
public class Application {
public Application(IPrint printProxy) {
printProxy.execute(10, " log print ");
printProxy.execute(0, " console print ");
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
複製程式碼
看上面的Application
的構造方法,要求傳入一個IPrint
引數,Spring會從容器中找到一個bean作為引數傳入,而這個bean就是我們生成的代理類,這樣才可以根據不同的引數來選中具體的實現類
所以問題就是如何宣告這個代理類了,配置如下,通過FactoryBean的方式來宣告Bean,並新增上@Primary
註解,這樣就可以確保注入的是我們宣告的代理類了
@Configuration
public class PrintAutoConfig {
@Bean
public SpiFactoryBean printSpiPoxy(ApplicationContext applicationContext) {
return new SpiFactoryBean(applicationContext, IPrint.class);
}
@Bean
@Primary
public IPrint printProxy(SpiFactoryBean spiFactoryBean) throws Exception {
return (IPrint) spiFactoryBean.getObject();
}
}
複製程式碼
上面的使用邏輯,涉及到的知識點在前面的博文中分別有過介紹,更多詳情可以參考
- FactoryBean的使用姿勢,參考:181009-SpringBoot基礎篇Bean之基本定義與使用
- 配置類
Configuration
宣告的方式,參考:181012-SpringBoot基礎篇Bean之自動載入 - @Primary註解的使用,參考: 181022-SpringBoot基礎篇Bean之多例項選擇
接下來就是實際執行看下結果如何了
III. 其他
0. 專案相關
a. 更多博文
基礎篇
- 181009-SpringBoot基礎篇Bean之基本定義與使用
- 181012-SpringBoot基礎篇Bean之自動載入
- 181013-SpringBoot基礎篇Bean之動態註冊
- 181018-SpringBoot基礎篇Bean之條件注入@Condition使用姿勢
- 181019-SpringBoot基礎篇Bean之@ConditionalOnBean與@ConditionalOnClass
- 181019-SpringBoot基礎篇Bean之條件注入@ConditionalOnProperty
- 181019-SpringBoot基礎篇Bean之條件注入@ConditionalOnExpression
- 181022-SpringBoot基礎篇Bean之多例項選擇
應用篇
b. 專案原始碼
- 工程:spring-boot-demo
- module: 000-spi-factorybean
1. 一灰灰Blog
- 一灰灰Blog個人部落格 blog.hhui.top
- 一灰灰Blog-Spring專題部落格 spring.hhui.top
一灰灰的個人部落格,記錄所有學習和工作中的博文,歡迎大家前去逛逛
2. 宣告
盡信書則不如,以上內容,純屬一家之言,因個人能力有限,難免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
3. 掃描關注
一灰灰blog