Dubbo中的SPI機制
概述
Service Provider Interface
即 SPI,是JDK內置的一種服務提供發現機制,可以用來啟用框架擴展和替換組件。可以讓不同的廠商針對統一接口編寫不同的實現
SPI實際上是“接口+策略模式+配置文件”實現的動態加載機制。在系統設計中,模塊之間通常基于接口編程,不直接顯示指定實現類。一旦代碼里指定了實現類,就無法在不修改代碼的情況下替換為另一種實現。為了達到動態可插拔的效果,java提供了SPI以實現服務發現。
SPI機制的應用場景有很多,我們比較常用的就是JDBC,Dubbo等
在談Dubbo的SPI機制前我們需要先了解一下Java原生的SPI機制
Java原生的SPI
- 首先我,我們先創建一個接口,接口中定義一個方法
package cuit.epoch.pymjl.spi;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:43**/
public interface Animal {/*** 動物叫*/void call();
}
- 然后我們分別寫兩個不同的實現類,實現這個接口
package cuit.epoch.pymjl.spi.impl;import cuit.epoch.pymjl.spi.Animal;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:44**/
public class Cat implements Animal {@Overridepublic void call() {System.out.println("貓叫:喵喵喵~");}
}
package cuit.epoch.pymjl.spi.impl;import cuit.epoch.pymjl.spi.Animal;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:45**/
public class Dog implements Animal {@Overridepublic void call() {System.out.println("狗叫:汪汪汪~");}
}
- 在項目
./resources/META-INF/servcie
下創建一個文件。文件名為你要動態擴展的接口的全限定名稱,我們這里就是Animaal
接口的全限定名稱,如圖所示:
文件里面的內容就是實現類的全限定名稱
cuit.epoch.pymjl.spi.impl.Cat
cuit.epoch.pymjl.spi.impl.Dog
- 編寫測試類
package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.util.Iterator;
import java.util.ServiceLoader;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:47**/
@Slf4j
public class Main {public static void main(String[] args) {log.info("這是Java原生的SPI機制");ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);for (Animal animal : serviceLoader) {animal.call();}}
}
- 運行測試
通過測試代碼可見,我們實現的兩個類成功的被加載并運行了相關的方法。但是我們并沒有在代碼中顯示的創建某個實現類,我們可以通過這種插件化的配置文件的形式實例化我們想要的對象。將程序的決定權解耦到配置文件中
但是Java原生的SPI也有其不足:
- 通過測試代碼我們可知,我們并不能直接指定加載某一個我們想要的類,只能通過遍歷加載配置文件中所有類來找到我們想要的類,這樣效率是很低的,造成資源的浪費
- 獲取類的方式不夠靈活,只能通過Iterator 形式獲取,不能根據某個參數來獲取對應的實現類。
- ServiceLoader線程不安全
針對以上不足,我們可以選擇Dubbo的SPI機制,以此來規避不足
Dubbo的SPI
Dubbo 并未使用 Java SPI,而是重新實現了一套功能更強的 SPI 機制。Dubbo SPI 的相關邏輯被封裝在了 ExtensionLoader 類中,通過 ExtensionLoader,我們可以加載指定的實現類。
另外,Dubbo是通過鍵值對的方式進行配置,我們可以直接通過Key獲取我們想要加載的實體類。Dubbo默認的配置文件路徑是在./resources/META-INF/dubbo
下
編寫測試方法:
package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.util.Iterator;
import java.util.ServiceLoader;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:47**/
@Slf4j
public class Main {public static void main(String[] args) {log.info("這是Dubbo的SPI機制");Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog");cat.call();dog.call();}
}
更改配置文件
cat=cuit.epoch.pymjl.spi.impl.Cat
dog=cuit.epoch.pymjl.spi.impl.Dog
運行測試:
通過代碼可知,Dubbo的SPI機制可直接通過Key獲取我們想要的對象實例,比原生的Java SPI更有優勢,除此之外,Dubbo在設計中還用到了大量的全局緩存,提高了我們實例化對象的效率,同時還支持通過注解默認實現,AOP,IOC等功能
下面是仿造Dubbo的源碼自定義實現的SPI機制的代碼,來自于Guide哥開源的RPC項目。雖然和源碼的有一些差別,也沒有Dubbo的功能完善,但核心思想還是一樣的
仿造源碼自定義實現Dubbo SPI機制
自定義注解MySpi
通過上文可知,Dubbo的SPI機制還要根據@spi
注解實現,對需要擴展的接口做一個標記,所以我們先自定義一個注解,用來標記我們想要擴展的接口
package cuit.epoch.pymjl.spi;import java.lang.annotation.*;/*** 標記接口,聲明該接口是一個擴展點** @author Pymjl* @version 1.0* @date 2022/5/3 12:05**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MySpi {//TODO value可以提供對默認實現的支持,但是這方面的切面并沒有寫,只是寫在這兒表示有這個東西String value() default "";
}
Holder對象
顧名思義,Holder就是持有通過ExtensionLoader加載來的實例化對象的對象
package cuit.epoch.pymjl.spi;/*** 持有人** @author Pymjl* @version 1.0* @date 2022/5/3 15:11**/
public class Holder<T> {private volatile T value;public T get() {return value;}public void set(T value) {this.value = value;}
}
ExtensionLoader
Dubbo SPI機制的核心類,使用它從文件中讀取配置,并通過反射實例化對象
package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 模仿Dubbo自定義擴展機制** @author Pymjl* @version 1.0* @date 2022/5/3 15:12**/
@Slf4j
public class ExtensionLoader<T> {/*** SPI配置文件根目錄*/private static final String SERVICE_DIRECTORY = "META-INF/diy-rpc/";/*** 本地緩存,Dubbo會先通過getExtensionLoader方法從緩存中獲取一個ExtensionLoader* 若緩存未命中,則會生成一個新的實例*/private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = new ConcurrentHashMap<>();/*** 目標擴展類的字節碼和實例對象*/private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();/*** 需要加載的擴展類類別*/private final Class<?> type;/*** 本地緩存*/private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();/*** 擴展類實例對象,key為配置文件中的key,value為實例對象的全限定名稱*/private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();public ExtensionLoader(Class<?> type) {this.type = type;}/*** 得到擴展加載程序** @param type 要擴展的接口,必須被MySpi標記* @return {@code ExtensionLoader<S>}*/public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {//判斷type是否為nullif (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}//如果不是接口if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}//判斷是否被MySpi標記if (type.getAnnotation(MySpi.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @MySpi");}//先從緩存中獲取擴展加載器,如果未命中,則創建ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);if (extensionLoader == null) {//未命中則創建,并放入緩存EXTENSION_LOADER_MAP.putIfAbsent(type, new ExtensionLoader<>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);}return extensionLoader;}/*** 得到擴展類對象實例** @param name 配置名字* @return {@code T}*/public T getExtension(String name) {//檢查參數if (name == null || name.trim().length() == 0) {throw new IllegalArgumentException("Extension name should not be null or empty.");}//先從緩存中獲取,如果未命中,新建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}//如果Holder還未持有目標對象,則為其創建一個單例對象Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {instance = createExtension(name);holder.set(instance);}}}return (T) instance;}/*** 通過擴展類字節碼創建實例對象** @param name 名字* @return {@code T}*/private T createExtension(String name) {//從文件中加載所有類型為 T 的擴展類并按名稱獲取特定的擴展類Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw new RuntimeException("No such extension of name " + name);}T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {try {EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();log.error(e.getMessage());}}return instance;}/*** 獲取所有擴展類** @return {@code Map<String, Class<?>>}*/private Map<String, Class<?>> getExtensionClasses() {//從緩存中獲取已經加載的擴展類Map<String, Class<?>> classes = cachedClasses.get();//雙重檢查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();//從配置文件中加載所有擴展類loadDirectory(classes);cachedClasses.set(classes);}}}return classes;}/*** 從配置目錄中加載所有擴展類** @param extensionsClasses 擴展類的K,V鍵值對*/private void loadDirectory(Map<String, Class<?>> extensionsClasses) {String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();try {//獲取配置文件的資源路徑Enumeration<URL> urls;ClassLoader classLoader = ExtensionLoader.class.getClassLoader();urls = classLoader.getResources(fileName);if (urls != null) {while (urls.hasMoreElements()) {URL resourceUrl = urls.nextElement();loadResource(extensionsClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}/*** 通過Url加載資源** @param extensionClasses 擴展類,key為配置文件中的key,Value為實現類的全限定名稱* @param classLoader 類加載器* @param resourceUrl 資源url*/private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), StandardCharsets.UTF_8))) {String line;//讀取文件中的每一行數據while ((line = reader.readLine()) != null) {//先排除配置文件中的注釋final int noteIndex = line.indexOf('#');//我們應該忽略掉注釋后的內容if (noteIndex > 0) {line = line.substring(0, noteIndex);}line = line.trim();if (line.length() > 0) {try {final int keyIndex = line.indexOf('=');String key = line.substring(0, keyIndex).trim();String value = line.substring(keyIndex + 1).trim();if (key.length() > 0 && value.length() > 0) {Class<?> clazz = classLoader.loadClass(value);extensionClasses.put(key, clazz);}} catch (ClassNotFoundException e) {e.printStackTrace();log.error(e.getMessage());}}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}
}
-
先聲明了一些常量,例如配置文件的根目錄,本地緩存等
/*** SPI配置文件根目錄*/private static final String SERVICE_DIRECTORY = "META-INF/diy-rpc/";/*** 本地緩存,Dubbo會先通過getExtensionLoader方法從緩存中獲取一個ExtensionLoader* 若緩存未命中,則會生成一個新的實例*/private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = new ConcurrentHashMap<>();/*** 目標擴展類的字節碼和實例對象*/private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();/*** 需要加載的擴展類類別*/private final Class<?> type;/*** 本地緩存*/private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();/*** 擴展類實例對象,key為配置文件中的key,value為實例對象的全限定名稱*/private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
-
ExtensionLoader 的getExtensionLoader 方法先從緩存中獲取一個ExtensionLoader 實例,若緩存未命中,則new一個。
/*** 得到擴展加載程序** @param type 要擴展的接口,必須被MySpi標記* @return {@code ExtensionLoader<S>}*/public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {//判斷type是否為nullif (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}//如果不是接口if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}//判斷是否被MySpi標記if (type.getAnnotation(MySpi.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @MySpi");}//先從緩存中獲取擴展加載器,如果未命中,則創建ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);if (extensionLoader == null) {//未命中則創建,并放入緩存EXTENSION_LOADER_MAP.putIfAbsent(type, new ExtensionLoader<>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);}return extensionLoader;}
-
然后再通過ExtensionLoader 的getExtension 方法獲取拓展類對象。跟上面的邏輯一樣,先從緩存中查找,若緩存未命中則新建一個
/*** 得到擴展類對象實例** @param name 配置名字* @return {@code T}*/public T getExtension(String name) {//檢查參數if (name == null || name.trim().length() == 0) {throw new IllegalArgumentException("Extension name should not be null or empty.");}//先從緩存中獲取,如果未命中,新建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}//如果Holder還未持有目標對象,則為其創建一個單例對象Object instance = holder.get();if (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {instance = createExtension(name);holder.set(instance);}}}return (T) instance;}/*** 通過擴展類字節碼創建實例對象** @param name 名字* @return {@code T}*/private T createExtension(String name) {//從文件中加載所有類型為 T 的擴展類并按名稱獲取特定的擴展類Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw new RuntimeException("No such extension of name " + name);}T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {try {EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);} catch (InstantiationException | IllegalAccessException e) {e.printStackTrace();log.error(e.getMessage());}}return instance;}/*** 獲取所有擴展類** @return {@code Map<String, Class<?>>}*/private Map<String, Class<?>> getExtensionClasses() {//從緩存中獲取已經加載的擴展類Map<String, Class<?>> classes = cachedClasses.get();//雙重檢查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();//從配置文件中加載所有擴展類loadDirectory(classes);cachedClasses.set(classes);}}}return classes;}/*** 從配置文件中加載所有擴展實現類** @param extensionsClasses 擴展類的K,V鍵值對*/private void loadDirectory(Map<String, Class<?>> extensionsClasses) {String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();try {//獲取配置文件的資源路徑Enumeration<URL> urls;ClassLoader classLoader = ExtensionLoader.class.getClassLoader();urls = classLoader.getResources(fileName);if (urls != null) {while (urls.hasMoreElements()) {URL resourceUrl = urls.nextElement();loadResource(extensionsClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}/*** 通過Url加載資源** @param extensionClasses 擴展類,key為配置文件中的key,Value為實現類的全限定名稱* @param classLoader 類加載器* @param resourceUrl 資源url*/private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), StandardCharsets.UTF_8))) {String line;//讀取文件中的每一行數據while ((line = reader.readLine()) != null) {//先排除配置文件中的注釋final int noteIndex = line.indexOf('#');//我們應該忽略掉注釋后的內容if (noteIndex > 0) {line = line.substring(0, noteIndex);}line = line.trim();if (line.length() > 0) {try {final int keyIndex = line.indexOf('=');String key = line.substring(0, keyIndex).trim();String value = line.substring(keyIndex + 1).trim();if (key.length() > 0 && value.length() > 0) {Class<?> clazz = classLoader.loadClass(value);extensionClasses.put(key, clazz);}} catch (ClassNotFoundException e) {e.printStackTrace();log.error(e.getMessage());}}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}
---------------------
作者:Pymj
來源:CSDN
原文:https://blog.csdn.net/apple_52109766/article/details/124577928
版權聲明:本文為作者原創文章,轉載請附上博文鏈接!
內容解析By:CSDN,CNBLOG博客文章一鍵轉載插件