更多Spring文章,歡迎點擊 一灰灰Blog-Spring專題java
FactoryBean在Spring中算是一個比較有意思的存在了,雖然在平常的業務開發中,基本上不怎麼會用到,但在某些場景下,若是用得好,卻能夠實現不少有意思的東西git
本篇博文主要介紹如何經過FactoryBean來實現一個類SPI機制的微型應用框架github
文章內涉及到的知識點spring
在看下面的內容以前,得知道一下什麼是SPI,以及SPI的用處和JDK實現SPI的方式,對於這一塊有興趣瞭解的童鞋,能夠看一下我的以前寫的相關文章編程
在開始以前,有必要了解一下,咱們準備作的這個東西,到底適用於什麼樣的場景。設計模式
在電商中,有一個比較恰當的例子,商品詳情頁的展現。拿淘寶系的詳情頁做爲背景來講明(沒有在阿里工做過,下面的東西純粹是爲了說明應用場景而展開)微信
假設有這麼三個詳情頁,咱們設定一個大前提,底層的數據層提供方都是一套的,商品詳情展現的服務徹底能夠作到複用,即三個性情頁中,絕大多數的東西都同樣,只是不一樣的詳情頁車重點不一樣而已。數據結構
如上圖中,咱們假定有細微區別的幾個地方app
位置 | 淘寶詳情 | 天貓詳情 | 鹹魚詳情 | 說明 |
---|---|---|---|---|
banner | 顯示淘寶的背景牆 | 顯示天貓的廣告位 | 鹹魚的坑位 | 三者數據結構徹底一致,僅圖片url不一樣 |
推薦 | 推薦同類商品 | 推薦店家其餘商品 | 推薦同類二手產品 | 數據結構相同,內容不一樣 |
評價 | 商品評價 | 商品評價 | 沒有評價,改成留言 | |
促銷 | 優惠券 | 天貓積分券 | 沒有券 | - |
根據上面的簡單對比,其實只想表達一個意思,業務基本上一致,僅僅只有不多的一些東西不一樣,須要定製化,這個時候能夠考慮用SPI來支持定製化的服務框架
SPI的全名爲Service Provider Interface,簡單的總結下java spi機制的思想。咱們系統裏抽象的各個模塊,每每有不少不一樣的實現方案,好比日誌模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對象的設計裏,咱們通常推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,若是須要替換一種實現,就須要修改代碼。爲了實如今模塊裝配的時候能不在程序裏動態指明,這就須要一種服務發現機制。 java spi就是提供這樣的一個機制:爲某個接口尋找服務實現的機制
上面是相對正視一點的介紹,簡單一點,符合本文設計目標的介紹以下
經過上面的描述,能夠發現一個最大的優勢就是:
一個簡單的應用場景以下
這個報警系統中,對於使用者而言,經過 IAlarm#sendMsg(level, msg)
來執行報警發送的方式,然而這一行的具體執行者是(忽略,日誌報警,郵件報警仍是短信報警)不肯定的,經過SPI的實現方式將是以下
若是咱們想新添加一種報警方式呢?那也很簡單,新建一個報警的實現
而後對於使用者而言,其餘的地方都不用改,只是在傳入的level參數換成5就能夠了
代理模式,在Spring中能夠說是很是很是很是常見的一種設計模式了,大名鼎鼎的AOP就是這個實現的一個經典case,常見的代理有兩種實現方式
簡單說一下,代理模式的定義和說明以下,更多詳情能夠參考: 實現MVC: 3. AOP實現準備篇代理模式
其實在現實生活中代理模式仍是很是多得,這裏引入一個代理商的概念來加以描述,原本一個水果園直接賣水果就行了,如今中間來了一個水果超市,水果園的代銷商,對水果進行分類,包裝,而後再賣給用戶,這其實也算是一種代理
百科定義:爲其餘對象提供一種代理以控制對這個對象的訪問。在某些狀況下,一個對象不適合或者不能直接引用另外一個對象,而代理對象能夠在客戶端和目標對象之間起到中介的做用。
瞭解完上面的前提以後,咱們能夠考慮下如何實現一個Spring容器中的SPI工具包
首先肯定大的生態環境爲Spring,咱們針對Bean作SPI功能的擴展,即定義一個SPI的接口,而後能夠有多個實現類,而且所有都聲明爲Bean;
SPI的一個重要特色就是能夠選中不一樣的實現來執行具體的代碼,那麼放在這裏,就會有兩種方案
方案對比
方案一 | 方案二 |
---|---|
接近JDK的SPI使用方式 | 代理方式選中匹配的實例 |
優勢:簡單,使用以及後續維護簡單 | 靈活, 支持更富想象力的擴展 |
缺點:一對一,複用性不夠,不能支持前面的case | 實現和調用方式跟繁瑣一點,須要傳入用於選擇具體實例條件參數 每次選擇子類都須要額外計算 |
對比上面的兩個方案以後,選中第二個(固然主要緣由是爲了演示FactoryBean和代理實現SPI機制,若是選擇方案一就沒有這兩個什麼事情了)
選中方案以後,目標拆分就比較清晰了
針對前面拆分的目標,進行方案設計,第一步就是接口相關的定義了
設計的SPI微型框架的核心爲:在執行的時候,根據傳入的參數來決定具體的實例來執行,所以咱們的接口設計中,至少有一個根據傳入的參數來判斷是否選中這個實例的接口
public interface ISpi<T> {
boolean verify(T condition);
}
複製代碼
看到上面的實現以後,就會有一個疑問,若是有多個子類都知足這個條件怎麼辦?所以能夠加一個排序的接口,返回優先級最高的匹配者
public interface ISpi<T> {
boolean verify(T condition);
/** * 排序,數字越小,優先級越高 * @return */
default int order() {
return 10;
}
}
複製代碼
接口定義以後,使用者應該怎麼用呢?
spi實現的約束
基於JDK的代理模式,一個最大的前提就是,只能根據接口來生成代理類,所以在使用SPI的時候,咱們但願使用者先定義一個接口來繼承ISpi
,而後具體的SPI實現這個接口便可
其次就是在Spring的生態下,要求全部的SPI實現都是Bean,須要自動掃描或者配置註解方式聲明,否者代理類就不太好獲取全部的SPI實現了
spi使用的約束
在使用SPI接口時,經過接口的方式來引入,由於咱們實際注入的會是代理類,所以不要寫具體的實現類
單獨看上面的說明,可能不太好理解,建議結合下面的實例演示對比
這個屬於最核心的地方了(雖然說重要性爲No1,但實現其實很是很是簡單)
代理類主要目的就是在具體調用執行時,根據傳入的參數來選中具體的執行者,執行後並返回對應的結果
org.springframework.beans.factory.ListableBeanFactory#getBeansOfType(java.lang.Class<T>)
)將上面的步驟具體實現,也就比較簡單了
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;
}
}
複製代碼
話說方案設計以後,應該就是實現了,然而由於實現過於簡單,設計的過程當中,也就順手寫了,就是上面的一個接口定義 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();
}
}
複製代碼
上面的使用邏輯,涉及到的知識點在前面的博文中分別有過介紹,更多詳情能夠參考
Configuration
聲明的方式,參考:181012-SpringBoot基礎篇Bean之自動加載接下來就是實際執行看下結果如何了
基礎篇
應用篇
一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激
一灰灰blog