java SPI機制的使用及原理

本片文章是針對dubbo SPI機制深入分析的平滑過渡的作用。當然咱們主要是學習優秀的思想,SPI就是一種解耦非常優秀的思想,我們可以思考在我們項目開發中是否可以使用、是否可以幫助我們解決某些問題、或者能夠更加提升項目的框架等

一、SPI是什麼

SPI(service provider interface)是java提供的一套用來被第三方實現或者擴展的API,它可以用來啓用框架擴展和替換組件。

如果用上面這句話來描述SPI那麼是一點卵用沒有,下面用生動的例子來闡述。其實SPI跟我們的策略設計模式比較相似,如果對策略設計模式不太瞭解的,可以先花點時間去學習一下。

實例:假如,我們在京東上購買商品需要付款,假如我們可以選擇的支付的模塊有支付寶、微信、銀行卡。如果我們使用策略設計模式的話,簡單的代碼如下。

/***
 * 抽象支付
 */
public interface Pay {

    void pay();
}
/**
 * @Auther:
 * @Date: 2020/5/2 14:28
 * @Description: 支付寶付款
 */
public class AliPay implements Pay {
    @Override
    public void pay() {
        System.out.println("使用支付寶pay....");
    }
}
/**
 * @Auther:
 * @Date: 2020/5/2 14:28
 * @Description: 微信pay
 */
public class WechatPay implements Pay {
    @Override
    public void pay() {
        System.out.println("使用微信支付....");
    }
}
/**
 * @Auther:
 * @Date: 2020/5/2 14:29
 * @Description:銀行卡pay
 */
public class BankCardPay implements Pay {
    @Override
    public void pay() {
        System.out.println("使用銀行卡支付....");
    }
}

你可以根據你的需求創建出相應的Pay的實現類,然後調用pay(),例如我簡寫一下

/**
 * @Auther:
 * @Date: 2020/5/2 14:36
 * @Description:
 */
public class Context {


    private final Pay pay;

    public Context(Pay pay) {
        this.pay = pay;
    }

    public void invokeStrategy(){
        pay.pay();
    }
}
/**
 * @Auther:
 * @Date: 2020/5/2 14:35
 * @Description:
 */
public class PayTest {

    public static void main(String[] args) {
        //ali
        Context aliContext = new Context(new AliPay());
        aliContext.invokeStrategy();
        //wechat
        Context wechatContext = new Context(new WechatPay());
        wechatContext.invokeStrategy();
    }
}

從上面的代碼中我們其實可以看到還是需要我們顯示的創建出相應的支付模塊。

二、SPI如何使用

那麼現在有這樣的場景:當我的項目裏面有什麼支付模塊我就使用什麼樣的支付模塊,比如說有支付寶支付模塊就選擇支付寶、有微信支付模塊我就選擇微信支付、同時有多個的時候,我默認選擇第一個,此時我們就可以使用SPI,先看下如何使用。

1、創建META-INF/services文件夾,然後創建一個以Pay接口全限定名爲名字的文件
在這裏插入圖片描述

2、在文件中編寫想要實現哪個Pay的實現類(AliPay,WechatPay,BankCardPay),注意也要是全限定名

假如是是支付寶支付的模塊,上面文件的內容:

com.taolong.dubbo.spi.strategy.AliPay

3、獲取Pay並調用

獲取並調用的邏輯,我就修改下上面的策略模式中的Context的invokerStrategy方法,這裏假設默認使用第一個

public void invokeStrategy(){
    ServiceLoader<Pay> payServiceLoader = ServiceLoader.load(Pay.class);
    Iterator<Pay> iterator = payServiceLoader.iterator();
    if (iterator.hasNext()){
        iterator.next().pay();
    }
}

main方法調用

 public static void main(String[] args) {
//        //ali
//        Context aliContext = new Context(new AliPay());
//        aliContext.invokeStrategy();
//        //wechat
//        Context wechatContext = new Context(new WechatPay());
//        wechatContext.invokeStrategy();
        Context context = new Context();
        context.invokeStrategy();
    }

上面就是使用的SPI機制,讓其選擇一個Pay的實現類,這樣子就比較靈活了,比如正常的團隊工作情況是下面這樣子。

A團隊:負責支付寶支付支付模塊的開發

B團隊:負責微信支付模塊的開發

C團隊:負責銀行卡支付模塊的開發

此時A團隊的支付模塊裏面只需要新建一個“com.taolong.dubbo.spi.strategy.Pay”的文件,文件的內容對應Alipay(他們自己命名),然後編寫自己的支付邏輯即可

B團隊也只需要新建“com.taolong.dubbo.spi.strategy.Pay”文件的內容比如是WechatPay(他們自己命名),然後編寫自己的邏輯即可。

相當於Pay定義了一個規範,不管是微信、還是支付寶支付只要符合這個規範就行。在使用的使用,我們只需要加入相應的依賴(比如支付寶模塊的pom依賴,微信模塊的pom依賴),項目就能自動發現具體的實現類,然後調用相應的模塊的支付方法。

三、SPI的優秀實現案例

如果對我上面的描述不太理解的話,我們來看一個真實的使用上述SPI的例子—數據庫驅動(Driver)

我們知道,當我們的項目裏面使用引用了mysql的驅動pom依賴時,我們的項目裏面會自動選擇使用mysql的驅動,我們甚至不需要手動去加載。我們來看看它的具體實現。

1、首先看一下java.sql.Driver的類

這裏面也是相當於定義了一個規範

2、其次看mysql驅動包的META-INF/services文件夾下面有沒有指定的文件

在這裏插入圖片描述
很熟悉,命名就是java.sql.Driver

3、打開文件查看一下文件內容

在這裏插入圖片描述
這裏面就能看到我們的mysql的驅動了,到這裏基本上就確認這也是使用SPI實現的,順便說一下,現在爲什麼我們不需要使用Class.forName()去加載驅動了,這是因爲DriverManager使用SPI的機制已經幫我們加載好了,我們來看看DriverManager的類

(1)靜態代碼塊

/**
 * Load the initial JDBC drivers by checking the System property
 * jdbc.properties and then use the {@code ServiceLoader} mechanism
 */
static {
    loadInitialDrivers();
    println("JDBC DriverManager initialized");
}

oadInitialDrivers()

private static void loadInitialDrivers() {
    String drivers;
    try {
        drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
            public String run() {
                return System.getProperty("jdbc.drivers");
            }
        });
    } catch (Exception ex) {
        drivers = null;
    }
    // If the driver is packaged as a Service Provider, load it.
    // Get all the drivers through the classloader
    // exposed as a java.sql.Driver.class service.
    // ServiceLoader.load() replaces the sun.misc.Providers()

    AccessController.doPrivileged(new PrivilegedAction<Void>() {
        public Void run() {

            //重點看這裏
            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            Iterator<Driver> driversIterator = loadedDrivers.iterator();

            /* Load these drivers, so that they can be instantiated.
             * It may be the case that the driver class may not be there
             * i.e. there may be a packaged driver with the service class
             * as implementation of java.sql.Driver but the actual class
             * may be missing. In that case a java.util.ServiceConfigurationError
             * will be thrown at runtime by the VM trying to locate
             * and load the service.
             *
             * Adding a try catch block to catch those runtime errors
             * if driver not available in classpath but it's
             * packaged as service and that service is there in classpath.
             */
            try{
                while(driversIterator.hasNext()) {
                    driversIterator.next();
                }
            } catch(Throwable t) {
            // Do nothing
            }
            return null;
        }
    });

    println("DriverManager.initialize: jdbc.drivers = " + drivers);

    if (drivers == null || drivers.equals("")) {
        return;
    }
    String[] driversList = drivers.split(":");
    println("number of Drivers:" + driversList.length);
    for (String aDriver : driversList) {
        try {
            println("DriverManager.Initialize: loading " + aDriver);
            Class.forName(aDriver, true,
                    ClassLoader.getSystemClassLoader());
        } catch (Exception ex) {
            println("DriverManager.Initialize: load failed: " + ex);
        }
    }
}

很明顯就能看到它的調用方法跟我們上面將的例子是一樣的,唯一不同的是它會加載所有的驅動。

不僅僅是數據庫驅動使用了SPI,還有slf4j、spring等等

上面的java的SPI的源碼本文限於篇幅,就不講解了,感興趣的可以自行閱讀,不過,我們也能夠猜測出它的主要的邏輯,下面用一副簡單的圖來描述一下

在這裏插入圖片描述
不管是文件名還是文件內容都是全限定名,所以通過反射很容易創建相應的類

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章