面試官問爛的 Dubbo中 SPI機(jī)制的運(yùn)行原理是什么?

SPI的全稱是(Service Provider Interface)是服務(wù)提供接口的意思。如果我們不寫框架性代碼或者開發(fā)插件的話,對(duì)于SPI機(jī)制可能不會(huì)那么熟悉,但如果我們閱讀諸如Dubbo、JDBC數(shù)據(jù)庫驅(qū)動(dòng)包、Spring以及最近比較流行的Spring Boot相關(guān)starter組件源碼的話,就會(huì)發(fā)現(xiàn)SPI機(jī)制及其思想在這些框架中有大量的應(yīng)用。

我們知道系統(tǒng)中抽象的各個(gè)模塊,往往會(huì)有很多不同的實(shí)現(xiàn)方案,例如我們常見的日志模塊方案、xml解析模塊JDBC驅(qū)動(dòng)模塊等。在面向?qū)ο蟮脑O(shè)計(jì)思想中,我們一般推薦模塊之間的對(duì)接基于面向接口的編程方式,而不是直接面向?qū)崿F(xiàn)類硬編碼。因?yàn)?,一旦代碼里涉及具體的實(shí)現(xiàn)類的耦合,就違反了可插拔、閉開等原則,如果需要替換一種實(shí)現(xiàn)方式,就需要修改代碼。如果我們希望實(shí)現(xiàn)在模塊裝配的時(shí)候能夠不在程序硬編碼指定,那就需要一種服務(wù)發(fā)現(xiàn)的機(jī)制(PS:不要和現(xiàn)在微服務(wù)的服務(wù)發(fā)現(xiàn)機(jī)制搞混淆了)。

JAVA中的SPI技術(shù)就是提供了這樣一個(gè)為某個(gè)接口尋找服務(wù)實(shí)現(xiàn)類的機(jī)制,這一點(diǎn)也類似于Spring框架中的IOC思想,就是將程序加載裝配的控制權(quán)移到程序之外,這個(gè)機(jī)制在組件模塊化設(shè)計(jì)中非常重要!那么在JAVA中SPI機(jī)制具體是如何約定的呢?

在JAVA SPI機(jī)制中約定,當(dāng)服務(wù)的提供者(例如某個(gè)新的日志組件),提供了服務(wù)接口的某種實(shí)現(xiàn)之后,在jar包的META-INF/services/目錄中同時(shí)創(chuàng)建一個(gè)以該服務(wù)接口命名的文件,文件中填寫了實(shí)現(xiàn)該服務(wù)接口具體實(shí)現(xiàn)類的全限定類名。這樣,在當(dāng)外部程序裝配這個(gè)模塊的時(shí)候,就可以通過該jar包中META-INF/services/目錄里的配置文件找到具體的實(shí)現(xiàn)類路徑,從而可以通過反射機(jī)制裝載實(shí)例化,從而完成模塊的注入。例如,我們Dubbo框架時(shí),除了引入核心依賴jar外,還會(huì)有很多擴(kuò)展組件如dubbo-monitor,如果我們需要引入此組件只需要簡單引入就可以,而不需要做額外的集成,主要原因就是因?yàn)樵摻M件時(shí)以SPI機(jī)制進(jìn)行的集成。

綜上所述,SPI機(jī)制實(shí)際上就是“基于接口的編程+策略模式+配置文件”組合實(shí)現(xiàn)的一種動(dòng)態(tài)加載機(jī)制,在JDK中提供了工具類:“java.util.ServiceLoader”來實(shí)現(xiàn)服務(wù)查找。

JDK SPI機(jī)制實(shí)現(xiàn)示例

JDK中自帶對(duì)SPI機(jī)制的支持,主要是涉及java.util.ServiceLoader類的使用,接下來,我們通過一個(gè)簡單的代碼示例來理解下JAVA中SPI機(jī)制的實(shí)現(xiàn)方式吧!我們先通過一張圖來看看使用JAVA SPI機(jī)制需要遵循什么規(guī)范吧:

└── src/main/java
├── cn
│   └── wudimanong
│       └── spi
│           ├── SPIService.java
│           ├── impl
│           │   ├── SpiImpl1.java
│           │   └── SpiImpl2.java
│           └── SPIMain.java
└── META-INF
    └── services
        └── com.wudimanong.spi.SPIService

1、我們需要在項(xiàng)目中創(chuàng)建目錄META-INF\services

2、定義一個(gè)接口服務(wù)提供,如SpiService

public interface SpiService {
    void execute();
}

3、分別定義兩個(gè)服務(wù)接口實(shí)現(xiàn)類,如:SpiImpl1,SpiImpl2

public class SpiImpl1 implements SPIService {
    @Override
    public void execute() {
        System.out.println("SpiImpl1 Hello.");
    }
}

// ------------------------------------------

public class SpiImpl2 implements SPIService {
    @Override
    public void execute() {
        System.out.println("SpiImpl2 Hello.");
    }
}

4、我們?cè)?code>ClassPath路徑下添加一個(gè)配置文件,文件的名稱是接口的全限定類名,內(nèi)容則是實(shí)現(xiàn)類的全限定類名,如果是多個(gè)實(shí)現(xiàn)類則用換行符分割,文件路徑如下

文件內(nèi)容如下:

cn.wudimanong.spi.impl.SpiImpl1
cn.wudimanong.spi.impl.SpiImpl2

這樣,我們基本上就遵循JAVA SPI的機(jī)制定義了組件基本結(jié)構(gòu),最后我們通過編寫測(cè)試類,看看如果使用SPI機(jī)制,客戶端代碼應(yīng)該如何寫:

public class SPIMain {
    public static void main(String[] args) {

        ServiceLoader<SPIService> loaders =
                ServiceLoader.load(SPIService.class);

        Iterator<SPIService> spiServiceIterator = loaders.iterator();
        System.out.println("classPath:" + System.getProperty("java.class.path"));
        while (spiServiceIterator.hasNext()) {
            SPIService spiService = spiServiceIterator.next();
            System.out.println(spiService.execute());
        }
    }
}

可以看到在引入方加載組件模塊時(shí)是通過ServiceLoader這個(gè)類來操作的,如果我們打開該類的源碼,就可以看到,其主要實(shí)現(xiàn)邏輯其實(shí)就是在META-INF/services/目錄中查找實(shí)現(xiàn)類,并進(jìn)行相關(guān)實(shí)例化操作。關(guān)于該類的部分關(guān)鍵源代碼如下:

public final class ServiceLoader<S> implements Iterable<S>{
    //配置文件的路徑
    private static final String PREFIX = "META-INF/services/";
    //加載的服務(wù)類或接口
    private final Class<S> service;
    //已加載的服務(wù)類集合
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    //類加載器
    private final ClassLoader loader;
    //內(nèi)部類,真正加載服務(wù)類
    private LazyIterator lookupIterator;

    public void reload() {
        //先清空
        providers.clear();
        //實(shí)例化內(nèi)部類
        lookupIterator = new LazyIterator(service, loader);
    }

    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //要加載的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //類加載器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //訪問控制器
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        //reload方法如上
        reload();
    }
}

而后面查找類和創(chuàng)建實(shí)現(xiàn)類的過程就都是在LazyIterator類完成的了,由于篇幅的關(guān)系,感興趣的朋友可以點(diǎn)開源碼自行閱讀一番!

JDBC數(shù)據(jù)庫驅(qū)動(dòng)包中SPI機(jī)制分析

通過上面的描述,相信大家對(duì)Java SPI機(jī)制的實(shí)現(xiàn)應(yīng)該是有了一個(gè)基本的認(rèn)識(shí),接下來我們以JDBC數(shù)據(jù)庫驅(qū)動(dòng)設(shè)計(jì)來看下Java SPI機(jī)制的真實(shí)應(yīng)用場(chǎng)景。我們知道通常各大數(shù)據(jù)庫廠商(如Mysql、Oracle)都會(huì)根據(jù)一個(gè)統(tǒng)一的規(guī)范,如:java.sql.Driver去開發(fā)各自的驅(qū)動(dòng)實(shí)現(xiàn)邏輯。

而我們?cè)谑褂玫膉dbc的時(shí)候客戶端卻是不需要改變代碼的,直接引入不同的SPI接口服務(wù)即可。例如以Mysql的JDBC驅(qū)動(dòng)jar來說:

這樣在引入mysql驅(qū)動(dòng)包后jdbc連接代碼java.sql.DriverManager,就會(huì)使用SPI機(jī)制來加載具體的jdbc實(shí)現(xiàn),關(guān)鍵源碼如下:

public class DriverManager {
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

    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;
        }

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

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                }
                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);
            }
        }
    }
}

可以看到粉紅色代碼部分,JDBC驅(qū)動(dòng)管理代碼就是通過原生的Java SPI機(jī)制來實(shí)現(xiàn)加載具體的Mysql JDBC驅(qū)動(dòng)的。

Dubbo框架中SPI機(jī)制分析

需要說明的是雖然Java 提供了對(duì)SPI機(jī)制的默認(rèn)實(shí)現(xiàn)支持,但是并不表示所有的框架都會(huì)默認(rèn)使用這種Java自帶的邏輯,SPI機(jī)制更多的是一種實(shí)現(xiàn)思想,而具體的實(shí)現(xiàn)邏輯,則是可以自己定義的。例如我們說Dubbo框架中大量使用了SPI技術(shù),但是Dubbo并沒有使用JDK原生的ServiceLoader,而是自己實(shí)現(xiàn)了ExtensionLoader來加載擴(kuò)展點(diǎn),所以我們看Dubbo框架源碼的時(shí)候,千萬不要被配置目錄是/META-INF/dubbo/internal,而不是META-INF/services/所迷惑了。

相應(yīng)地如果其他框架中也使用了自定義的SPI機(jī)制實(shí)現(xiàn),也不要疑惑,它們也只是重新定義了類似于ServiceLoader類的加載邏輯而已,其背后的設(shè)計(jì)思想和原理則都是一樣的!例如,以Dubbo中卸載協(xié)議的代碼舉例:

 private void destroyProtocols() {
        ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
        for (String protocolName : loader.getLoadedExtensions()) {
            try {
                Protocol protocol = loader.getLoadedExtension(protocolName);
                if (protocol != null) {
                    protocol.destroy();
                }
            } catch (Throwable t) {
                logger.warn(t.getMessage(), t);
            }
        }
    }

而ExtensionLoader中掃描的配置路徑如下:

public class ExtensionLoader<T> {

    private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class);

    private static final String SERVICES_DIRECTORY = "META-INF/services/";

    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

    private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();

    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
}

以上通過對(duì)Java自帶SPI機(jī)制的示例以及對(duì)Dubbo和JDBC驅(qū)動(dòng)框架中對(duì)SPI機(jī)制應(yīng)用的分析,相信大家應(yīng)該是有了一個(gè)總體的原理性的認(rèn)識(shí)了。如果需要更加深入的了解一些細(xì)節(jié)的實(shí)現(xiàn)邏輯就需要大家好好去看看ServiceLoader的源碼了,如果其他框架單獨(dú)實(shí)現(xiàn)了SPI機(jī)制,其相應(yīng)的實(shí)現(xiàn)加載工具類也需要具體看看它們的源碼是怎么實(shí)現(xiàn)的了!

原文地址

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容