面試重點: 來說說Dubbo SPI 機制
SPI是什么
SPI是一種簡稱,全名叫 Service Provider Interface,Java本身提供了一套SPI機制,SPI 的本質是將接口實現(xiàn)類的全限定名配置在文件中,并由服務加載器讀取配置文件,加載實現(xiàn)類,這樣可以在運行時,動態(tài)為接口替換實現(xiàn)類,這也是很多框架組件實現(xiàn)擴展功能的一種手段。
而今天要說的Dubbo SPI機制和Java SPI還是有一點區(qū)別的,Dubbo 并未使用 Java 原生的 SPI 機制,而是對他進行了改進增強,進而可以很容易地對Dubbo進行功能上的擴展。
學東西得帶著問題去學,我們先提幾個問題,再接著看
1.什么是SPI(開頭已經(jīng)解釋了)
2.Dubbo SPI和Java原生的有什么區(qū)別
3.兩種實現(xiàn)應該如何寫出來
Java SPI是如何實現(xiàn)的
先定義一個接口:
- public interface Car {
- void startUp();
- }
然后創(chuàng)建兩個類,都實現(xiàn)這個Car接口
- 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文件夾下創(chuàng)建一個名稱為接口的全限定名,com.example.demo.spi.Car。
文件內容寫上實現(xiàn)類的全限定名,如下:
- 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);
- }
- }
執(zhí)行完的輸出結果:
- The train started
- The truck started
Dubbo SPI是如何實現(xiàn)的
Dubbo 使用的SPI并不是Java原生的,而是重新實現(xiàn)了一套,其主要邏輯都在ExtensionLoader類中,邏輯也不難,后面會稍帶講一下
看看使用,和Java的差不了太多,基于前面的例子來看下,接口類需要加上@SPI注解:
- @SPI
- public interface Car {
- void startUp();
- }
實現(xiàn)類不需要改動
配置文件需要放在META-INF/dubbo下面,配置寫法有些區(qū)別,直接看代碼:
- 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();
- }
- }
執(zhí)行結果:
- The train started
Dubbo SPI中常用的注解
- @SPI 標記為擴展接口
- @Adaptive自適應拓展實現(xiàn)類標志
- @Activate 自動激活條件的標記
總結一下兩者區(qū)別:
- 使用上的區(qū)別Dubbo使用ExtensionLoader而不是ServiceLoader了,其主要邏輯都封裝在這個類中
- 配置文件存放目錄不一樣,Java的在META-INF/services,Dubbo在META-INF/dubbo,META-INF/dubbo/internal
- Java SPI 會一次性實例化擴展點所有實現(xiàn),如果有擴展實現(xiàn)初始化很耗時,并且又用不上,會造成大量資源被浪費
- Dubbo SPI 增加了對擴展點 IOC 和 AOP 的支持,一個擴展點可以直接 setter 注入其它擴展點
- Java SPI加載過程失敗,擴展點的名稱是拿不到的。比如:JDK 標準的 ScriptEngine,getName() 獲取腳本類型的名稱,如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類加載失敗,這個失敗原因是不會有任何提示的,當用戶執(zhí)行 ruby 腳本時,會報不支持 ruby,而不是真正失敗的原因
前面的3個問題是不是已經(jīng)能回答出來了?是不是非常簡單
Dubbo SPI源碼分析
Dubbo SPI使用上是通過ExtensionLoader的getExtensionLoader方法獲取一個 ExtensionLoader 實例,然后再通過 ExtensionLoader 的 getExtension 方法獲取拓展類對象。這其中,getExtensionLoader 方法用于從緩存中獲取與拓展類對應的 ExtensionLoader,如果沒有緩存,則創(chuàng)建一個新的實例,直接上代碼:
- public T getExtension(String name) {
- if (name == null || name.length() == 0) {
- throw new IllegalArgumentException("Extension name == null");
- }
- if ("true".equals(name)) {
- // 獲取默認的拓展實現(xiàn)類
- 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) {
- // 創(chuàng)建擴展實例
- instance = createExtension(name);
- // 設置實例到 holder 中
- holder.set(instance);
- }
- }
- }
- return (T) instance;
- }
上面這一段代碼主要做的事情就是先檢查緩存,緩存不存在創(chuàng)建擴展對象
接下來我們看看創(chuàng)建的過程:
- 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) {
- // 反射創(chuàng)建實例
- EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
- instance = (T) EXTENSION_INSTANCES.get(clazz);
- }
- // 向實例中注入依賴
- injectExtension(instance);
- Set<Class<?>> wrapperClasses = cachedWrapperClasses;
- if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
- // 循環(huán)創(chuàng)建 Wrapper 實例
- for (Class<?> wrapperClass : wrapperClasses) {
- // 將當前 instance 作為參數(shù)傳給 Wrapper 的構造方法,并通過反射創(chuàng)建 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.反射創(chuàng)建對象
3.給擴展類注入依賴
4.將擴展類對象包裹在對應的Wrapper對象里面
我們在通過名稱獲取擴展類之前,首先需要根據(jù)配置文件解析出擴展類名稱到擴展類的映射關系表,之后再根據(jù)擴展項名稱從映射關系表中取出相應的拓展類即可。相關過程的代碼如下:
- 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();
- // 根據(jù)文件名加載所有的同名文件
- 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 方法加載資源。我們繼續(xù)跟下去,看一下 loadResource 方法的實現(xiàn)
- 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 數(shù)組的第一個元素作為鍵,
- // 存儲 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方法操作了不同的緩存,比如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等等
到這里基本上關于緩存類加載的過程就分析完了,其他邏輯不難,認真地讀下來加上Debug一下都能看懂的。
總結
從設計思想上來看的話,SPI是對迪米特法則和開閉原則的一種實現(xiàn)。
開閉原則:對修改關閉對擴展開放。這個原則在眾多開源框架中都非常常見,Spring的IOC容器也是大量使用。
迪米特法則:也叫最小知識原則,可以解釋為,不該直接依賴關系的類之間,不要依賴;有依賴關系的類之間,盡量只依賴必要的接口。
那Dubbo的SPI為什么不直接使用Spring的呢,這一點從眾多開源框架中也許都能窺探一點端倪出來,因為本身作為開源框架是要融入其他框架或者一起運行的,不能作為依賴被依賴對象存在。
再者對于Dubbo來說,直接用Spring IOC AOP的話有一些架構臃腫,完全沒必要,所以自己實現(xiàn)一套輕量級反而是最優(yōu)解
本文轉載自微信公眾號「 架構技術專欄」,可以通過以下二維碼關注。轉載本文請聯(lián)系 架構技術專欄公眾號。