1. 程式人生 > >SpringBoot應用篇之FactoryBean及代理實現SPI機制示例

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

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輔助說明

一個簡單的應用場景如下

報警系統demo

這個報警系統中,對於使用者而言,通過 IAlarm#sendMsg(level, msg) 來執行報警傳送的方式,然而這一行的具體執行者是(忽略,日誌報警,郵件報警還是簡訊報警)不確定的,通過SPI的實現方式將是如下

  • 如果level為1,則忽略報警內容
  • 如果level為2,則採用日誌報警的方式來報警

如果我們想新新增一種報警方式呢?那也很簡單,新建一個報警的實現

  • level == 5, 則採用微信報警

然後對於使用者而言,其他的地方都不用改,只是在傳入的level引數換成5就可以了

3. 代理模式簡述

代理模式,在Spring中可以說是非常非常非常常見的一種設計模式了,大名鼎鼎的AOP就是這個實現的一個經典case,常見的代理有兩種實現方式

  • JDK方式
  • CGLIB方式

其實在現實生活中代理模式還是非常多得,這裡引入一個代理商的概念來加以描述,本來一個水果園直接賣水果就好了,現在中間來了一個水果超市,水果園的代銷商,對水果進行分類,包裝,然後再賣給使用者,這其實也算是一種代理

百科定義:為其他物件提供一種代理以控制對這個物件的訪問。在某些情況下,一個物件不適合或者不能直接引用另一個物件,而代理物件可以在客戶端和目標物件之間起到中介的作用。

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或者有更好的建議,歡迎批評指正,不吝感激

  • QQ: 一灰灰/3302797840

3. 掃描關注

一灰灰blog

QrCode

知識星球

goals

相關推薦

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

FactoryBean在Spring中算是一個比較有意思的存在了,雖然在日常的業務開發中,基本上不怎麼會用到,但在某些場景下,如果用得好,卻可以實現很多有意思的東西 本篇博文主要介紹如何通過FactoryBean來實現一個類SPI機制的微型應用框架 文章內涉及

SpringBoot系列教程應用藉助Redis搭建一個簡單站點統計服務

判斷一個網站值不值錢的一個重要標準就是看pv/uv,那麼你知道pv,uv是怎麼統計的麼?當然現在有第三方做的比較完善的可以直接使用

LVM基本應用,擴展縮減實現

查看 size reduce swa The 創建文件 nbsp AC from LVM概述   【百度百科】 LVM是邏輯盤卷管理(LogicalVolumeManager)的簡稱,它是Linux環境下對磁盤分區進行管理的一種機制,LVM是建立在硬盤和 分區之上的一個邏輯

LVM基本應用,擴展縮減實現

家目錄 新建 默認 直接 基本 splay snapshot 自己 組成 LVM是使用純軟件的方式來組織一個或多個底層硬件設備為一個抽象的邏輯設備來使用的這一解決方案。它用到了內核中的dm模塊:     dm: device mapper,將一個或多個底層設備組織成一個邏輯

iOS總結-Runtime用途面試題的總結字典模型互換

字典轉模型 許多第三方的字典轉model,如MJExtension,都是利用runtime進行轉換的。 下面也是簡單的實現,真正用到的基本上就是NSArray/NSDictionary/NSString/基本資料型別,各種巢狀,其中主要實現邏輯對NSArray和NSDictionary拆開單

iOS總結-Runtime用途面試題的總結Associated Object關聯物件

Associated Object關聯物件也是runtime應用裡面的一種. 由於category_t的結構體裡面沒有objc_ivar_list,所以無法新增成員變數,可以通過@property來新增屬性,但是 分類中@property生成屬性,並不能自動生成setter/getter方法

iOS總結-Runtime用途面試題的總結一

runtime的用途基本分下面幾類: 實現多繼承Multiple Inheritance 上一篇裡面的最後利用methodSignatureForSelector來進行一次轉發,在forwardInvocation方法裡,Person將是send的方法轉移到可以實現send方法的gender

Spring Cloud 應用 Spring Cloud Config(配置中心)

為了方便儲存,轉自:https://blog.csdn.net/hubo_88/article/details/80692156 從前幾篇文章中我們就可以看出,在分散式系統中,服務數量會很多,如果要修改服務的配置檔案,會很麻煩,這個時候,我們想把配置檔案放在一個地方統一管理,實時更新,Sprin

Android提高第十五ListView自適應實現表格

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

spark系列-應用通過yarn api提交Spark任務

前言 在工作中,大部使用的都是hadoop和spark的shell命令,或者通過java或者scala編寫程式碼。最近工作涉及到通過yarn api處理spark任務,感覺yarn的api還是挺全面的,但是呼叫時需要傳入很多引數,而且會出現一些詭異的問題。雖然

Android Fk: PKMS(3)installdLocalSocket實現Java層與Native層通訊

LOCAL_CLANG := true#Android Fk: PKMS(3)之installd及LocalSocket實現Java層與Native層通訊 一、installd的概述   從上一篇介紹應用安裝與解除安裝的學習文件中知道PKMS在實現部分包管理功能時需要藉助instal

jQuery jsonp跨域請求 ajax跨域---伺服器端代理實現

  ajax跨域之---伺服器端代理實現 https://www.cnblogs.com/lydialee/p/4869145.html   來源   https://www.cnblogs.com/chiangchou/p/jsonp.html

springboot整合html分頁功能實現

說不清楚,直接上程式碼。和展示效果:前臺:<!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"      xmlns:sec

二維凸包convex hullC++OpenCV實現

打算接下來好好研究下演算法(很明顯,演算法才是王道啊),然後儘量用直觀的方式輸出,於是用OpenCV畫圖成了不二首選,各位看官接下來看到一堆“XXX之C++及OpenCV實現”之類的標題就別見怪了~ 另外還有個打算,看到自己寫的東西被別人拿去佔為己有,不爽,開始貼版權

樹莓派3B應用視訊監控(中)

上篇說到相機的拍照、錄影測試以及實現定時拍照、錄影。 這一篇,我來說說更強大的一款軟體,motion,他可以進行運動捕捉,同時可以將監控的畫面實時傳送出來。 一、motion的安裝 使用apt命

層次聚類AGNESPython實現

層次聚類 層次聚類,顧名思義,就是一層一層的進行聚類,它試圖在不同層次對資料集進行劃分,可以由上向下把大的類別分割,即“自頂向下”的分拆策略(見下面AGNES部分),也可以由下向上對小的類別進行聚合,即“自底向下”的聚合策略:開始把所有的樣本都歸為一類,然後逐

python3.6urllib模組代理實現

1.代理伺服器地址選擇網址:http://www.xicidaili.com import urllib.request #代理伺服器網址 "http://www.xicidaili.com" proxy_handler = urllib.request.Proxy

Spring Cloud 應用 Hystrix Dashboard(斷路器監控) 的基本搭建

在以往的文章裡,已經講解了 斷路器 Hystrix 的基本使用,現在將介紹斷路器的監控 Hystrix Dashboard 的基本搭建。(一)簡介Hystrix Dashboard 是 Hystrix 的儀表盤元件,提供了資料監控,可以實時監控 Hystrix 的各個指標,然

資料結構Java實現

一、棧的基本介紹 棧是一種只允許在一端進行插入或刪除的線性表,也就是說先進後出。棧的操作端通常被稱為棧頂,另一端被稱為棧底,棧的插入操作稱為壓棧(push),棧刪除操作稱為出棧(pop)。壓棧是把新元素放到棧頂元素的上面,使之成為新的棧頂元素;出棧則是把棧頂元

Spring Cloud 應用 Spring Cloud Sleuth + Zipkin(三)修改資料儲存方式

(一)簡介預設情況下,Zipkin Server 會將跟蹤資訊儲存在記憶體中,每次重啟 Zipkin Server 都會使之前收集的跟蹤資訊丟失,並且當有大量跟蹤資訊時,記憶體儲存也會造成效能瓶頸,所以通常我們都需要將跟蹤資訊儲存到外部元件中,如 Mysql。由於 Sprin