SpringBoot應用篇之FactoryBean及代理實現SPI機制示例

更多Spring文章,歡迎點擊 一灰灰Blog-Spring專題java

FactoryBean在Spring中算是一個比較有意思的存在了,雖然在平常的業務開發中,基本上不怎麼會用到,但在某些場景下,若是用得好,卻能夠實現不少有意思的東西git

本篇博文主要介紹如何經過FactoryBean來實現一個類SPI機制的微型應用框架github

文章內涉及到的知識點spring

  • SPI機制
  • FactoryBean
  • JDK動態代理

I. 相關知識點

在看下面的內容以前,得知道一下什麼是SPI,以及SPI的用處和JDK實現SPI的方式,對於這一塊有興趣瞭解的童鞋,能夠看一下我的以前寫的相關文章編程

1. demo背景說明

在開始以前,有必要了解一下,咱們準備作的這個東西,到底適用於什麼樣的場景。設計模式

在電商中,有一個比較恰當的例子,商品詳情頁的展現。拿淘寶系的詳情頁做爲背景來講明(沒有在阿里工做過,下面的東西純粹是爲了說明應用場景而展開)微信

商品詳情頁

假設有這麼三個詳情頁,咱們設定一個大前提,底層的數據層提供方都是一套的,商品詳情展現的服務徹底能夠作到複用,即三個性情頁中,絕大多數的東西都同樣,只是不一樣的詳情頁車重點不一樣而已。數據結構

如上圖中,咱們假定有細微區別的幾個地方app

位置 淘寶詳情 天貓詳情 鹹魚詳情 說明
banner 顯示淘寶的背景牆 顯示天貓的廣告位 鹹魚的坑位 三者數據結構徹底一致,僅圖片url不一樣
推薦 推薦同類商品 推薦店家其餘商品 推薦同類二手產品 數據結構相同,內容不一樣
評價 商品評價 商品評價 沒有評價,改成留言
促銷 優惠券 天貓積分券 沒有券 -

根據上面的簡單對比,其實只想表達一個意思,業務基本上一致,僅僅只有不多的一些東西不一樣,須要定製化,這個時候能夠考慮用SPI來支持定製化的服務框架

2. SPI簡述

a. 基本定義

SPI的全名爲Service Provider Interface,簡單的總結下java spi機制的思想。咱們系統裏抽象的各個模塊,每每有不少不一樣的實現方案,好比日誌模塊的方案,xml解析模塊、jdbc模塊的方案等。面向的對象的設計裏,咱們通常推薦模塊之間基於接口編程,模塊之間不對實現類進行硬編碼。一旦代碼裏涉及具體的實現類,就違反了可拔插的原則,若是須要替換一種實現,就須要修改代碼。爲了實如今模塊裝配的時候能不在程序裏動態指明,這就須要一種服務發現機制。 java spi就是提供這樣的一個機制:爲某個接口尋找服務實現的機制

上面是相對正視一點的介紹,簡單一點,符合本文設計目標的介紹以下

  • 接口方式引用
  • 具體執行時,根據某些條件,選中實際的子類執行

經過上面的描述,能夠發現一個最大的優勢就是:

  • 經過擴展接口的實現,就能夠實現服務擴展;而不須要改原來的業務代碼

b. demo輔助說明

一個簡單的應用場景以下

報警系統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();
    }
}
複製代碼

上面的使用邏輯,涉及到的知識點在前面的博文中分別有過介紹,更多詳情能夠參考

接下來就是實際執行看下結果如何了

演示demo

III. 其餘

0. 項目相關

a. 更多博文

基礎篇

應用篇

b. 項目源碼

1. 一灰灰Blog

一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛

2. 聲明

盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現bug或者有更好的建議,歡迎批評指正,不吝感激

3. 掃描關注

一灰灰blog

QrCode

相關文章
相關標籤/搜索