Dubbo(二):深入理解Dubbo的服務發現SPI機制
一、前言
用到微服務就不得不來談談服務發現的話題。通俗的來說,就是在提供服務方把服務註冊到註冊中心,並且告訴服務消費方現在已經存在了這個服務。那麼裡面的細節到底是怎麼通過程式碼實現的呢,現在我們來看看Dubbo中的SPI機制
二、SPI簡介
SPI 全稱為 Service Provider Interface,是一種服務發現機制。SPI本質是將介面實現類的全限定名配置在檔案中,並由服務載入器讀取配置檔案,載入實現類,這樣執行時可以動態的為介面替換實現類
三、Dubbo中的SPI
Dubbo與上面的普通的Java方式實現SPI不同,在Dubbo中重新實現了一套功能更強的SPI機制,即通過鍵值對的方式進行配置及快取。其中也使用ConcurrentHashMap與synchronize防止併發問題出現。主要邏輯封裝在ExtensionLoader中。下面我們看看原始碼。
四、ExtensionLoader原始碼解析
由於內部的方法實在太多,我們只挑選與實現SPI的重要邏輯部分拿出來講解。
1、getExtensionLoader(Class<T> type)
1 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { 2 if (type == null) { 3 throw new IllegalArgumentException("Extension type == null"); 4 } else if (!type.isInterface()) { 5 throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!"); 6 } else if (!withExtensionAnnotation(type)) { 7 throw new IllegalArgumentException("Extension type (" + type + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!"); 8 } else { 9 ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type); 10 if (loader == null) { 11 EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); 12 loader = (ExtensionLoader)EXTENSION_LOADERS.get(type); 13 } 14 15 return loader; 16 } 17 }
這個是可以將對應的介面轉換為ExtensionLoader 例項。相當於告訴Dubbo這是個服務介面,裡面有對應的服務提供者
先是邏輯判斷傳進來的類不能為空,必須是介面且被@SPI註解註釋過。這三個條件都滿足就會建立ExtensionLoader 例項。同樣的,如果當前類已經被建立過ExtensionLoader 例項,那麼直接拿取。否則新建一個。這裡使用的是鍵值對的儲存型別,如下圖:
使用ConcurrentHashMap防止在併發時出現問題,並且效率高HashTable不少,所以我們日常專案併發場景中也應該多用ConcurrentHashMap進行儲存。
2、getExtension(String name)
1 public T getExtension(String name) { 2 if (name == null || name.length() == 0) 3 throw new IllegalArgumentException("Extension name == null"); 4 if ("true".equals(name)) { 5 // 獲取預設的拓展實現類 6 return getDefaultExtension(); 7 } 8 // Holder,顧名思義,用於持有目標物件 9 Holder<Object> holder = cachedInstances.get(name); 10 if (holder == null) { 11 cachedInstances.putIfAbsent(name, new Holder<Object>()); 12 holder = cachedInstances.get(name); 13 } 14 Object instance = holder.get(); 15 // 雙重檢查 16 if (instance == null) { 17 synchronized (holder) { 18 instance = holder.get(); 19 if (instance == null) { 20 // 建立拓展例項 21 instance = createExtension(name); 22 // 設定例項到 holder 中 23 holder.set(instance); 24 } 25 } 26 } 27 return instance; 28 }View Code
這個方法主要是相當於得到具體的服務,上述我們已經對服務的介面進行載入,現在我們需要呼叫服務介面下的某一個具體服務實現類。就用這個方法。上述方法可以看出是會進入getOrCreateHolder中,這個方法顧名思義是獲取或者建立Holder。進入到下面方法中:
1 private Holder<Object> getOrCreateHolder(String name) { 2 //檢查快取中是否存在 3 Holder<Object> holder = (Holder)this.cachedInstances.get(name); 4 if (holder == null) { 5 //快取中不存在就去建立一個新的Holder 6 this.cachedInstances.putIfAbsent(name, new Holder()); 7 holder = (Holder)this.cachedInstances.get(name); 8 } 9 10 return holder; 11 }View Code
同樣,快取池也是以ConcurrentHashMap為儲存結構
3、createExtension(String name)
實際上getExtension方法不一定每次都能拿到,當服務實現類是第一次進行載入的時候就需要當前的方法
1 private T createExtension(String name) { 2 Class<?> clazz = (Class)this.getExtensionClasses().get(name); 3 if (clazz == null) { 4 throw this.findException(name); 5 } else { 6 try { 7 T instance = EXTENSION_INSTANCES.get(clazz); 8 if (instance == null) { 9 EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); 10 instance = EXTENSION_INSTANCES.get(clazz); 11 } 12 13 this.injectExtension(instance); 14 Set<Class<?>> wrapperClasses = this.cachedWrapperClasses; 15 Class wrapperClass; 16 if (CollectionUtils.isNotEmpty(wrapperClasses)) { 17 for(Iterator var5 = wrapperClasses.iterator(); var5.hasNext(); instance = this.injectExtension(wrapperClass.getConstructor(this.type).newInstance(instance))) { 18 wrapperClass = (Class)var5.next(); 19 } 20 } 21 22 return instance; 23 } catch (Throwable var7) { 24 throw new IllegalStateException("Extension instance (name: " + name + ", class: " + this.type + ") couldn't be instantiated: " + var7.getMessage(), var7); 25 } 26 } 27 }View Code
可以看出createExtension實際上是一個私有方法,也就是由上面的getExtension自動觸發。內部邏輯大致為:
3.1、通過 getExtensionClasses 獲取所有的拓展類
3.2、通過反射建立拓展物件
3.3、向拓展物件中注入依賴(這裡Dubbo有單獨的IOC後面會介紹)
3.4、將拓展物件包裹在相應的 Wrapper 物件中
4、getExtensionClasses()
1 private Map<String, Class<?>> getExtensionClasses() { 2 // 從快取中獲取已載入的拓展類 3 Map<String, Class<?>> classes = cachedClasses.get(); 4 // 雙重檢查 5 if (classes == null) { 6 synchronized (cachedClasses) { 7 classes = cachedClasses.get(); 8 if (classes == null) { 9 // 載入拓展類 10 classes = loadExtensionClasses(); 11 cachedClasses.set(classes); 12 } 13 } 14 } 15 return classes; 16 } 17 18 //進入到loadExtensionClasses中 19 20 private Map<String, Class<?>> loadExtensionClasses() { 21 // 獲取 SPI 註解,這裡的 type 變數是在呼叫 getExtensionLoader 方法時傳入的 22 final SPI defaultAnnotation = type.getAnnotation(SPI.class); 23 if (defaultAnnotation != null) { 24 String value = defaultAnnotation.value(); 25 if ((value = value.trim()).length() > 0) { 26 // 對 SPI 註解內容進行切分 27 String[] names = NAME_SEPARATOR.split(value); 28 // 檢測 SPI 註解內容是否合法,不合法則丟擲異常 29 if (names.length > 1) { 30 throw new IllegalStateException("more than 1 default extension name on extension..."); 31 } 32 33 // 設定預設名稱,參考 getDefaultExtension 方法 34 if (names.length == 1) { 35 cachedDefaultName = names[0]; 36 } 37 } 38 } 39 40 Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>(); 41 // 載入指定資料夾下的配置檔案 42 loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY); 43 loadDirectory(extensionClasses, DUBBO_DIRECTORY); 44 loadDirectory(extensionClasses, SERVICES_DIRECTORY); 45 return extensionClasses; 46 } 47 48 //進入到loadDirectory中 49 50 private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) { 51 // fileName = 資料夾路徑 + type 全限定名 52 String fileName = dir + type.getName(); 53 try { 54 Enumeration<java.net.URL> urls; 55 ClassLoader classLoader = findClassLoader(); 56 // 根據檔名載入所有的同名檔案 57 if (classLoader != null) { 58 urls = classLoader.getResources(fileName); 59 } else { 60 urls = ClassLoader.getSystemResources(fileName); 61 } 62 if (urls != null) { 63 while (urls.hasMoreElements()) { 64 java.net.URL resourceURL = urls.nextElement(); 65 // 載入資源 66 loadResource(extensionClasses, classLoader, resourceURL); 67 } 68 } 69 } catch (Throwable t) { 70 logger.error("..."); 71 } 72 } 73 74 //進入到loadResource中 75 76 private void loadResource(Map<String, Class<?>> extensionClasses, 77 ClassLoader classLoader, java.net.URL resourceURL) { 78 try { 79 BufferedReader reader = new BufferedReader( 80 new InputStreamReader(resourceURL.openStream(), "utf-8")); 81 try { 82 String line; 83 // 按行讀取配置內容 84 while ((line = reader.readLine()) != null) { 85 // 定位 # 字元 86 final int ci = line.indexOf('#'); 87 if (ci >= 0) { 88 // 擷取 # 之前的字串,# 之後的內容為註釋,需要忽略 89 line = line.substring(0, ci); 90 } 91 line = line.trim(); 92 if (line.length() > 0) { 93 try { 94 String name = null; 95 int i = line.indexOf('='); 96 if (i > 0) { 97 // 以等於號 = 為界,擷取鍵與值 98 name = line.substring(0, i).trim(); 99 line = line.substring(i + 1).trim(); 100 } 101 if (line.length() > 0) { 102 // 載入類,並通過 loadClass 方法對類進行快取 103 loadClass(extensionClasses, resourceURL, 104 Class.forName(line, true, classLoader), name); 105 } 106 } catch (Throwable t) { 107 IllegalStateException e = new IllegalStateException("Failed to load extension class..."); 108 } 109 } 110 } 111 } finally { 112 reader.close(); 113 } 114 } catch (Throwable t) { 115 logger.error("Exception when load extension class..."); 116 } 117 } 118 119 //進入到loadClass中 120 121 private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, 122 Class<?> clazz, String name) throws NoSuchMethodException { 123 124 if (!type.isAssignableFrom(clazz)) { 125 throw new IllegalStateException("..."); 126 } 127 128 // 檢測目標類上是否有 Adaptive 註解 129 if (clazz.isAnnotationPresent(Adaptive.class)) { 130 if (cachedAdaptiveClass == null) { 131 // 設定 cachedAdaptiveClass快取 132 cachedAdaptiveClass = clazz; 133 } else if (!cachedAdaptiveClass.equals(clazz)) { 134 throw new IllegalStateException("..."); 135 } 136 137 // 檢測 clazz 是否是 Wrapper 型別 138 } else if (isWrapperClass(clazz)) { 139 Set<Class<?>> wrappers = cachedWrapperClasses; 140 if (wrappers == null) { 141 cachedWrapperClasses = new ConcurrentHashSet<Class<?>>(); 142 wrappers = cachedWrapperClasses; 143 } 144 // 儲存 clazz 到 cachedWrapperClasses 快取中 145 wrappers.add(clazz); 146 147 // 程式進入此分支,表明 clazz 是一個普通的拓展類 148 } else { 149 // 檢測 clazz 是否有預設的構造方法,如果沒有,則丟擲異常 150 clazz.getConstructor(); 151 if (name == null || name.length() == 0) { 152 // 如果 name 為空,則嘗試從 Extension 註解中獲取 name,或使用小寫的類名作為 name 153 name = findAnnotationName(clazz); 154 if (name.length() == 0) { 155 throw new IllegalStateException("..."); 156 } 157 } 158 // 切分 name 159 String[] names = NAME_SEPARATOR.split(name); 160 if (names != null && names.length > 0) { 161 Activate activate = clazz.getAnnotation(Activate.class); 162 if (activate != null) { 163 // 如果類上有 Activate 註解,則使用 names 陣列的第一個元素作為鍵, 164 // 儲存 name 到 Activate 註解物件的對映關係 165 cachedActivates.put(names[0], activate); 166 } 167 for (String n : names) { 168 if (!cachedNames.containsKey(clazz)) { 169 // 儲存 Class 到名稱的對映關係 170 cachedNames.put(clazz, n); 171 } 172 Class<?> c = extensionClasses.get(n); 173 if (c == null) { 174 // 儲存名稱到 Class 的對映關係 175 extensionClasses.put(n, clazz); 176 } else if (c != clazz) { 177 throw new IllegalStateException("..."); 178 } 179 } 180 } 181 } 182 }View Code
上面的方法較多,理一下邏輯:
1、getExtensionClasses():先檢查快取,若快取未命中,則通過 synchronized 加鎖。加鎖後再次檢查快取,並判斷是否為空。此時如果 classes 仍為 null,則通過 loadExtensionClasses 載入拓展類。
2、loadExtensionClasses():對 SPI 註解的介面進行解析,而後呼叫 loadDirectory 方法載入指定資料夾配置檔案。
3、loadDirectory():方法先通過 classLoader 獲取所有資源連結,然後再通過 loadResource 方法載入資源。
4、loadResource():用於讀取和解析配置檔案,並通過反射載入類,最後呼叫 loadClass 方法進行其他操作。loadClass 方法用於主要用於操作快取。
5、小結:
我們稍微捋一下Dubbo是如何進行SPI的即發現介面的實現類。先是需要例項化擴充套件類載入器。這裡為了更好的和微服務貼合起來,我們就把它稱作服務載入器。在服務載入器中用的是ConcurrentHashMap的快取結構。在我們需要尋找服務的過程中,Dubbo先通過反射載入類,而後將有@SPI表示的介面(即服務介面)的實現類(即服務提供方)進行配置對應的資料夾及檔案。將配置檔案以鍵值對的方式存到快取中key就是當前服務介面下類的名字,value就是Dubbo生成的對應的類配置檔案。方便我們下次呼叫。其中為了防止併發問題產生,使用ConcurrentHashMap,並且使用synchronize關鍵字對存在併發問題的節點進行雙重檢查。
五、Dubbo中的IOC
在createExtension中有提到過將拓展物件注入依賴。這裡使用的是injectExtension(T instance):
1 private T injectExtension(T instance) { 2 try { 3 if (objectFactory != null) { 4 // 遍歷目標類的所有方法 5 for (Method method : instance.getClass().getMethods()) { 6 // 檢測方法是否以 set 開頭,且方法僅有一個引數,且方法訪問級別為 public 7 if (method.getName().startsWith("set") 8 && method.getParameterTypes().length == 1 9 && Modifier.isPublic(method.getModifiers())) { 10 // 獲取 setter 方法引數型別 11 Class<?> pt = method.getParameterTypes()[0]; 12 try { 13 // 獲取屬性名,比如 setName 方法對應屬性名 name 14 String property = method.getName().length() > 3 ? 15 method.getName().substring(3, 4).toLowerCase() + 16 method.getName().substring(4) : ""; 17 // 從 ObjectFactory 中獲取依賴物件 18 Object object = objectFactory.getExtension(pt, property); 19 if (object != null) { 20 // 通過反射呼叫 setter 方法設定依賴 21 method.invoke(instance, object); 22 } 23 } catch (Exception e) { 24 logger.error("fail to inject via method..."); 25 } 26 } 27 } 28 } 29 } catch (Exception e) { 30 logger.error(e.getMessage(), e); 31 } 32 return instance; 33 }View Code
在上面程式碼中,objectFactory 變數的型別為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 列表,用於儲存其他型別的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用於建立自適應的拓展,後者是用於從 Spring 的 IOC 容器中獲取所需的拓展。這就是我們常說的Dubbo為什麼能夠與Spring無縫連線,因為Dubbo底層就是依賴Spring的,對於Spring的IOC容器可直接拿來用。
六、總結
從框架的原始碼中如果要繼續深挖的話,可以多思考思考synchronize用的地方,為什麼要用,如果不用的話會有什麼併發問題。Dubbo的服務發現只是為我們以後學習Dubbo框架打下基礎,至少讓我們知道Dubbo是如何進行服務發現的。