面試重點: 來講說Dubbo SPI 機制

SPI是什麼

SPI是一種簡稱,全名叫 Service Provider Interface,Java自己提供了一套SPI機制,SPI 的本質是將接口實現類的全限定名配置在文件中,並由服務加載器讀取配置文件,加載實現類,這樣能夠在運行時,動態爲接口替換實現類,這也是不少框架組件實現擴展功能的一種手段。java

而今天要說的Dubbo SPI機制和Java SPI仍是有一點區別的,Dubbo 並未使用 Java 原生的 SPI 機制,而是對他進行了改進加強,進而能夠很容易地對Dubbo進行功能上的擴展。web

學東西得帶着問題去學,咱們先提幾個問題,再接着看數組

1.什麼是SPI(開頭已經解釋了)緩存

2.Dubbo SPI和Java原生的有什麼區別ruby

3.兩種實現應該如何寫出來微信

Java SPI是如何實現的

先定義一個接口:架構

public interface Car {
 void startUp();
}

而後建立兩個類,都實現這個Car接口app

public class Truck implements Car{
 @Override
 public void startUp() {
  System.out.println("The truck started");
 }
}

public class Train implements Car{
 @Override
 public void startUp() {
  System.out.println("The train started");
 }
}

而後在項目META-INF/services文件夾下建立一個名稱爲接口的全限定名,com.example.demo.spi.Car框架

文件內容寫上實現類的全限定名,以下:編輯器

com.example.demo.spi.Train
com.example.demo.spi.Truck

最後寫一個測試代碼:

public class JavaSPITest {
 @Test
 public void testCar() {
  ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
  serviceLoader.forEach(Car::startUp);
 }
}

執行完的輸出結果:

The train started
The truck started

Dubbo SPI是如何實現的

Dubbo 使用的SPI並非Java原生的,而是從新實現了一套,其主要邏輯都在ExtensionLoader類中,邏輯也不難,後面會稍帶講一下

看看使用,和Java的差不了太多,基於前面的例子來看下,接口類須要加上@SPI註解:

@SPI
public interface Car {
 void startUp();
}

實現類不須要改動

配置文件須要放在META-INF/dubbo下面,配置寫法有些區別,直接看代碼:

train = com.example.demo.spi.Train
truck = com.example.demo.spi.Truck

最後就是測試類了,先看代碼:

public class JavaSPITest {
 @Test
 public void testCar() {
  ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
  Car car = extensionLoader.getExtension("train");
  car.startUp();
 }
}

執行結果:

The train started

Dubbo SPI中經常使用的註解

  • @SPI 標記爲擴展接口

  • @Adaptive自適應拓展實現類標誌

  • @Activate 自動激活條件的標記

總結一下二者區別:

  • 使用上的區別Dubbo使用 ExtensionLoader而不是 ServiceLoader了,其主要邏輯都封裝在這個類中
  • 配置文件存放目錄不同,Java的在 META-INF/services,Dubbo在 META-INF/dubboMETA-INF/dubbo/internal
  • Java SPI 會一次性實例化擴展點全部實現,若是有擴展實現初始化很耗時,而且又用不上,會形成大量資源被浪費
  • Dubbo SPI 增長了對擴展點 IOC 和 AOP 的支持,一個擴展點能夠直接 setter 注入其它擴展點
  • Java SPI加載過程失敗,擴展點的名稱是拿不到的。好比:JDK 標準的 ScriptEngine,getName() 獲取腳本類型的名稱,若是 RubyScriptEngine 由於所依賴的 jruby.jar 不存在,致使 RubyScriptEngine 類加載失敗,這個失敗緣由是不會有任何提示的,當用戶執行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的緣由

前面的3個問題是否是已經能回答出來了?是否是很是簡單

Dubbo SPI源碼分析

Dubbo SPI使用上是經過ExtensionLoadergetExtensionLoader方法獲取一個 ExtensionLoader 實例,而後再經過 ExtensionLoadergetExtension 方法獲取拓展類對象。這其中,getExtensionLoader 方法用於從緩存中獲取與拓展類對應的 ExtensionLoader,若是沒有緩存,則建立一個新的實例,直接上代碼:

public T getExtension(String name) {
    if (name == null || name.length() == 0) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        // 獲取默認的拓展實現類
        return getDefaultExtension();
    }
    // 用於持有目標對象
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // DCL
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 建立擴展實例
                instance = createExtension(name);
                // 設置實例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

上面這一段代碼主要作的事情就是先檢查緩存,緩存不存在建立擴展對象

接下來咱們看看建立的過程:

private T createExtension(String name) {
    // 從配置文件中加載全部的擴展類,可獲得「配置項名稱」到「配置類」的映射關係表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 反射建立實例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向實例中注入依賴
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循環建立 Wrapper 實例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 將當前 instance 做爲參數傳給 Wrapper 的構造方法,並經過反射建立 Wrapper 實例。
                // 而後向 Wrapper 實例中注入依賴,最後將 Wrapper 實例再次賦值給 instance 變量
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
                    type + ") couldn't be instantiated: " + t.getMessage(), t);
    }
}

這段代碼看着繁瑣,其實也不難,一共只作了4件事情:

1.經過getExtensionClasses獲取全部配置擴展類

2.反射建立對象

3.給擴展類注入依賴

4.將擴展類對象包裹在對應的Wrapper對象裏面

咱們在經過名稱獲取擴展類以前,首先須要根據配置文件解析出擴展類名稱到擴展類的映射關係表,以後再根據擴展項名稱從映射關係表中取出相應的拓展類便可。相關過程的代碼以下:

private Map<String, Class<?>> getExtensionClasses() {
    // 從緩存中獲取已加載的拓展類
    Map<String, Class<?>> classes = cachedClasses.get();
    // DCL
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加載擴展類
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

這裏也是先檢查緩存,若緩存沒有,則經過一次雙重鎖檢查緩存,判空。此時若是 classes 仍爲 null,則經過 loadExtensionClasses 加載拓展類。下面是 loadExtensionClasses 方法的代碼

private Map<String, Class<?>> loadExtensionClasses() {
    // 獲取 SPI 註解,這裏的 type 變量是在調用 getExtensionLoader 方法時傳入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 對 SPI 註解內容進行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 檢測 SPI 註解內容是否合法,不合法則拋出異常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }

            // 設置默認名稱,參考 getDefaultExtension 方法
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // 加載指定文件夾下的配置文件
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

loadExtensionClasses 方法總共作了兩件事情,一是對 SPI 註解進行解析,二是調用 loadDirectory 方法加載指定文件夾配置文件。SPI 註解解析過程比較簡單,無需多說。下面咱們來看一下 loadDirectory 作了哪些事情

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件夾路徑 + type 全限定名 
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        // 根據文件名加載全部的同名文件
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // 加載資源
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                    type + ", description file: " + fileName + ").", t);
    }
}

loadDirectory 方法先經過 classLoader 獲取全部資源連接,而後再經過 loadResource 方法加載資源。咱們繼續跟下去,看一下 loadResource 方法的實現

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
                          java.net.URL resourceURL)
 
{
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // 按行讀取配置內容
            while ((line = reader.readLine()) != null) {
                // 定位 # 字符
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // 截取 # 以前的字符串,# 以後的內容爲註釋,須要忽略
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // 以等於號 = 爲界,截取鍵與值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加載類,並經過 loadClass 方法對類進行緩存
                            loadClass(extensionClasses, resourceURL, 
                                      Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e =
                          new IllegalStateException("Failed to load extension class...");
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class...");
    }
}

loadResource 方法用於讀取和解析配置文件,並經過反射加載類,最後調用 loadClass 方法進行其餘操做。loadClass 方法用於主要用於操做緩存,該方法的邏輯以下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, 
    Class<?> clazz, String name)
 throws NoSuchMethodException 
{
    
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("...");
    }

    // 檢測目標類上是否有 Adaptive 註解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // 設置 cachedAdaptiveClass緩存
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("...");
        }
        
    // 檢測 clazz 是不是 Wrapper 類型
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        // 存儲 clazz 到 cachedWrapperClasses 緩存中
        wrappers.add(clazz);
        
    // 程序進入此分支,代表 clazz 是一個普通的拓展類
    } else {
        // 檢測 clazz 是否有默認的構造方法,若是沒有,則拋出異常
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // 若是 name 爲空,則嘗試從 Extension 註解中獲取 name,或使用小寫的類名做爲 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("...");
            }
        }
        // 切分 name
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                // 若是類上有 Activate 註解,則使用 names 數組的第一個元素做爲鍵,
                // 存儲 name 到 Activate 註解對象的映射關係
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存儲 Class 到名稱的映射關係
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    // 存儲名稱到 Class 的映射關係
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}

綜上,loadClass方法操做了不一樣的緩存,好比cachedAdaptiveClasscachedWrapperClassescachedNames等等

到這裏基本上關於緩存類加載的過程就分析完了,其餘邏輯不難,認真地讀下來加上Debug一下都能看懂的。

總結

從設計思想上來看的話,SPI是對迪米特法則和開閉原則的一種實現。

開閉原則:對修改關閉對擴展開放。這個原則在衆多開源框架中都很是常見,Spring的IOC容器也是大量使用。

迪米特法則:也叫最小知識原則,能夠解釋爲,不應直接依賴關係的類之間,不要依賴;有依賴關係的類之間,儘可能只依賴必要的接口。

那Dubbo的SPI爲何不直接使用Spring的呢,這一點從衆多開源框架中也許都能窺探一點端倪出來,由於自己做爲開源框架是要融入其餘框架或者一塊兒運行的,不能做爲依賴被依賴對象存在。

再者對於Dubbo來講,直接用Spring IOC  AOP的話有一些架構臃腫,徹底不必,因此本身實現一套輕量級反而是最優解


往期推薦

還在爲大數據平臺搭建而煩惱嗎?一柄神器送給你

還在爲 Arthas 命令頭疼? 來看看這個插件吧!

Spring Cloud認證受權系列(一)基礎概念

基礎坑!新版Mac Big Sur 幹翻了個人Nacos

講真!這些攻擊手段你知道嗎



本文分享自微信公衆號 - 架構技術專欄(jiagoujishu)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索