擴展點開發指南
1 簡介
SPI 的全名是 Service Provider Interface,是一種服務發現機制。SPI 的本質是在檔案中配置介面實現類的全限定名,服務載入器讀取設定檔並載入實現類。通過這種方式,可以在執行時動態地替換介面的實現類。由於這個特性,我們可以通過 SPI 機制輕鬆地為程式提供擴充功能。SPI 機制也用於第三方框架中。例如,Dubbo 通過 SPI 機制載入所有元件。然而,Dubbo 並未使用 Java 原生的 SPI 機制,而是對其進行了增強,以更好地滿足需求。在 Dubbo 中,SPI 是一個非常重要的模組。基於 SPI,我們可以輕鬆擴展 Dubbo。在 Dubbo 中,SPI 主要有兩種用法,一種是載入固定的擴充類,另一種是載入自適應的擴充類。下面將詳細介紹這兩種方法。需要注意的是:在 Dubbo 中,基於 SPI 擴充載入的類都是單例的。
1.1 載入固定擴充類
如果讓你設計並載入固定的擴充類,你會怎麼做?一個常見的思路是在特定目錄下讀取設定檔,然後解析出完整的類名,透過反射機制實例化類,再將類儲存在集合中。需要用的時候,直接從集合中取出。Dubbo 中的實現也是這樣的思路。不過,在 Dubbo 中,實現更加完善,它實現了 IOC 和 AOP 的功能。IOC 指的是,如果這個擴充類依賴其他的屬性,Dubbo 會自動注入這個屬性。這個功能是如何實現的呢?一個常見的思路是,取得這個擴充類的 setter 方法,呼叫 setter 方法進行屬性注入。AOP 指的是什麼呢?指的是 Dubbo 可以將它的包裝類注入到擴充類中。例如,DubboProtocol 是 Protocol 的一個擴充類,ProtocolListenerWrapper 是 DubboProtocol 的一個包裝類。
1.2 載入自適應擴充類
首先說明一下自適應擴展類的使用場景。例如,我們有一個需求,在調用某個方法時,可以根據參數的選擇調用不同的實現類。有點類似工廠方法,根據不同的參數構造不同的實例對象。Dubbo 中的實現思路與此類似,但 Dubbo 的實現更加靈活,它的實現方式有點類似策略模式。每個擴展類相當於一個策略,基於 URL 消息總線,將參數傳遞給 ExtensionLoader,通過 ExtensionLoader 根據參數加載相應的擴展類,實現運行時對目標實例的動態調用。
2. Dubbo SPI 源碼分析
2.1 加載固定的擴展類
在 Dubbo 中,SPI 加載固定擴展類的入口是 ExtensionLoader 的 getExtension 方法。接下來,我們將詳細分析獲取擴展類對象的過程。
public T getExtension(String name) {
if (name == null || name. length() == 0)
throw new IllegalArgumentException("Extension name == null");
if ("true".equals(name)) {
// Get the default extension implementation class
return getDefaultExtension();
}
// Holder, as the name suggests, is used to hold the target object
Holder<Object> holder = cachedInstances. get(name);
// This logic ensures that only one thread can create a Holder object
if (holder == null) {
cachedInstances. putIfAbsent(name, new Holder<Object>());
holder = cachedInstances. get(name);
}
Object instance = holder. get();
// double check
if (instance == null) {
synchronized (holder) {
instance = holder. get();
if (instance == null) {
// create extension instance
instance = createExtension(name);
// set the instance to the holder
holder.set(instance);
}
}
}
return (T) instance;
}
上面程式碼的邏輯比較簡單,首先檢查緩存,如果緩存未命中,則創建擴展對象。我們來看看創建擴展對象的過程。
private T createExtension(String name, boolean wrap) {
// Load all extension classes from the configuration file to get the mapping relationship table from "configuration item name" to "configuration class"
Class<?> clazz = getExtensionClasses().get(name);
// If there is no extension of the interface, or the implementation class of the interface does not allow repetition but actually repeats, an exception is thrown directly
if (clazz == null || unacceptableExceptions. contains(name)) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES. get(clazz);
// This code ensures that the extended class will only be constructed once, which is a singleton.
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
instance = (T) EXTENSION_INSTANCES. get(clazz);
}
// Inject dependencies into the instance
injectExtension(instance);
// Automatically wrap if wrapping is enabled.
// For example, I defined the extension of DubboProtocol based on Protocol, but in fact, DubboProtocol is not directly used in Dubbo, but its wrapper class
// ProtocolListenerWrapper
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator. COMPARATOR);
Collections. reverse(wrapperClassesList);
}
// Loop to create Wrapper instances
if (CollectionUtils. isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass. getAnnotation(Wrapper. class);
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
// Pass the current instance as a parameter to the constructor of Wrapper, and create a Wrapper instance through reflection.
// Then inject dependencies into the Wrapper instance, and finally assign the Wrapper instance to the instance variable again
instance = injectExtension((T) wrapperClass. getConstructor(type). newInstance(instance));
}
}
}
}
// initialization
initExtension(instance);
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t. getMessage(), t);
}
}
createExtension 方法的邏輯稍微複雜一些,包含以下幾個步驟:
- 通過 getExtensionClasses 獲取所有擴展類
- 通過反射創建擴展對象
- 向擴展對象注入依賴
- 將擴展對象包裝到對應的 Wrapper 對象中
- 初始化擴展對象
在以上步驟中,第一步是加載擴展類的關鍵,第三步和第四步是 Dubbo IOC 和 AOP 的具體實現。在後面的章節中,我們將重點分析 getExtensionClasses 方法的邏輯,並簡要介紹 Dubbo IOC 的具體實現。
2.1.1 獲取所有擴展類
在我們根據名稱獲取擴展類之前,首先需要根據配置文件解析出擴展項名稱到擴展類的映射關係表(Map<名稱, 擴展類>),然後再根據擴展項名稱從映射關係表中取出對應的擴展類即可。相關過程的程式碼分析如下:
private Map<String, Class<?>> getExtensionClasses() {
// Get the loaded extension class from the cache
Map<String, Class<?>> classes = cachedClasses. get();
// double check
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses. get();
if (classes == null) {
// load extension class
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
這裡也是先檢查緩存,如果緩存未命中,則通過 synchronized 加鎖。加鎖後再次檢查緩存,發現為空。此時如果 classes 仍然為 null,則通過 loadExtensionClasses 加載擴展類。我們來分析一下 loadExtensionClasses 方法的邏輯。
private Map<String, Class<?>> loadExtensionClasses() {
// Cache the default SPI extension
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
// Load the files in the specified folder based on the policy
// Currently there are four strategies to read the configuration files in META-INF/services/ META-INF/dubbo/ META-INF/dubbo/internal/ META-INF/dubbo/external/ respectively
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}
loadExtensionClasses 方法總共做了兩件事情,一是解析 SPI 註解,二是調用 loadDirectory 方法加載指定資料夾的配置文件。解析 SPI 註解的過程比較簡單,這裡就不多說了。我們來看看 loadDirectory 做了什麼。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
// fileName = folder path + type fully qualified name
String fileName = dir + type;
try {
Enumeration<java.net. URL> urls = null;
ClassLoader classLoader = findClassLoader();
// try to load from ExtensionLoader's ClassLoader first
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader. class. getClassLoader();
if (ClassLoader. getSystemClassLoader() != extensionLoaderClassLoader) {
urls = extensionLoaderClassLoader. getResources(fileName);
}
}
// Load all files with the same name according to the file name
if (urls == null || !urls.hasMoreElements()) {
if (classLoader != null) {
urls = classLoader. getResources(fileName);
} else {
urls = ClassLoader. getSystemResources(fileName);
}
}
if (urls != null) {
while (urls. hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
// load resources
loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
}
}
} 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, boolean overridden, String... excludedPackages) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL. openStream(), StandardCharsets. UTF_8))) {
String line;
String clazz = null;
// Read configuration content line by line
while ((line = reader. readLine()) != null) {
// locate # characters
final int ci = line. indexOf('#');
if (ci >= 0) {
// Intercept the string before #, the content after # is a comment, which needs to be ignored
line = line. substring(0, ci);
}
line = line. trim();
if (line. length() > 0) {
try {
String name = null;
// Use the equal sign = as the boundary to intercept the key and value
int i = line. indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
clazz = line.substring(i + 1).trim();
} else {
clazz = line;
}
// Load the class and cache the class through the loadClass method
if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
loadClass(extensionClasses, resourceURL, Class. forName(clazz, true, classLoader), name, overridden);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
exceptions. put(line, e);
}
}
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
loadResource 方法用於讀取和解析配置文件,通過反射加載類別,最後調用 loadClass 方法進行其他操作。loadClass 方法主要用於操作緩存,該方法的邏輯如下:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
boolean overridden) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " +
type + ", class line: " + clazz. getName() + "), class "
+ clazz.getName() + " is not subtype of interface.");
}
// Check if there are Adaptive annotations on the target class
if (clazz. isAnnotationPresent(Adaptive. class)) {
cacheAdaptiveClass(clazz, overridden);
} else if (isWrapperClass(clazz)) {
// cache wrapper class
cacheWrapperClass(clazz);
} else {
// Enter here, indicating that this class is just an ordinary extension class
// Check if clazz has a default constructor, if not, throw an exception
clazz. getConstructor();
if (StringUtils. isEmpty(name)) {
// If name is empty, try to get name from Extension annotation, or use lowercase class name as name
name = findAnnotationName(clazz);
if (name. length() == 0) {
throw new IllegalStateException("No such extension name for the class " + clazz. getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR. split(name);
if (ArrayUtils. isNotEmpty(names)) {
// If the class has the Activate annotation, use the first element of the names array as the key,
// Store the mapping relationship between name and Activate annotation object
cacheActivateClass(clazz, names[0]);
for (String n : names) {
// Store the mapping relationship from Class to name
cacheName(clazz, n);
// Store the mapping relationship from name to Class.
// If there are multiple implementation classes corresponding to the same extension, whether overriding is allowed based on the override parameter, if not, an exception is thrown.
saveInExtensionClass(extensionClasses, clazz, n, overridden);
}
}
}
}
如上所示,loadClass 方法操作不同的緩存,例如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等。除此之外,該方法沒有其他邏輯。
至此,類別加載緩存過程的分析就結束了。整個過程沒有什麼特別複雜的地方,可以一步一步分析,不懂的地方可以除錯一下。接下來,我們來聊聊 Dubbo IOC。
2.1.2 Dubbo IOC
Dubbo IOC 透過 setter 方法注入依賴。Dubbo 首先透過反射取得實例的所有方法,然後遍歷方法清單,檢測方法名稱是否具有 setter 方法的特徵。如果是,則透過 ObjectFactory 取得依賴物件,最後透過反射呼叫 setter 方法,將依賴設定到目標物件中。整個過程對應的程式碼如下
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
// traverse all methods of the target class
for (Method method : instance. getClass(). getMethods()) {
// Check whether the method starts with set, and the method has only one parameter, and the method access level is public
if (!isSetter(method)) {
continue;
}
/**
* Detect whether there is DisableInject annotation modification.
*/
if (method. getAnnotation(DisableInject. class) != null) {
continue;
}
/**
* Detect whether the ScopeModelAware and ExtensionAccessorAware classes are implemented, and if implemented, do not inject
*/
if (method. getDeclaringClass() == ScopeModelAware. class) {
continue;
}
if (instance instanceof ScopeModelAware || instance instanceof ExtensionAccessorAware) {
if (ignoredInjectMethodsDesc. contains(ReflectUtils. getDesc(method))) {
continue;
}
}
// Primitive types are not injected
Class<?> pt = method. getParameterTypes()[0];
if (ReflectUtils. isPrimitives(pt)) {
continue;
}
try {
// Get the attribute name, for example, the setName method corresponds to the attribute name name
String property = getSetterProperty(method);
// Get dependent objects from ObjectFactory
Object object = objectFactory. getExtension(pt, property);
if (object != null) {
// inject
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type. getName() + ": " + e. getMessage(), e);
}
}
} catch (Exception e) {
logger. error(e. getMessage(), e);
}
return instance;
}
在上述程式碼中,objectFactory 變數的類型為 AdaptiveExtensionFactory,AdaptiveExtensionFactory 內部維護了一個 ExtensionFactory 清單,用於存放其他類型的 ExtensionFactory。Dubbo 目前提供了兩種 ExtensionFactory,分別是 SpiExtensionFactory 和 SpringExtensionFactory。前者用於建立自適應擴展,後者用於從 Spring 的 IOC 容器中取得所需的擴展。這兩個類的程式碼並不複雜,這裡就不逐一分析了。
Dubbo IOC 目前只支援 setter 注入。總體來說,邏輯比較簡單,容易理解。
2.2 載入自適應擴展類
自適應擴展類的含義是,基於參數,在執行時動態選擇某個具體的目標類,然後執行。在 Dubbo 中,很多擴展都是透過 SPI 機制載入的,例如 Protocol、Cluster、LoadBalance 等。有時候,有些擴展不希望在框架啟動階段就被載入,而是希望在呼叫擴展方法的時候,根據執行時參數進行載入。這聽起來有點矛盾,如果擴展沒有被載入,那麼擴展方法就無法被呼叫(靜態方法除外);如果擴展方法沒有被呼叫,那麼擴展就無法被載入。對於這個矛盾的問題,Dubbo 透過自適應擴展機制很好地解決了。自適應擴展機制的實現邏輯比較複雜,首先 Dubbo 會為擴展介面生成具有代理功能的程式碼。然後透過 javassist 或 jdk 編譯這段程式碼,得到 Class 類。最後透過反射建立代理類,整個流程比較繁瑣。
載入自適應擴展類的入口是 ExtensionLoader 的 getAdaptiveExtension 方法。
public T getAdaptiveExtension() {
// Get the adaptive extension from the cache
Object instance = cachedAdaptiveInstance. get();
if (instance == null) {
// If there is an exception, throw it directly
if (createAdaptiveInstanceError != null) {
throw new IllegalStateException("Failed to create adaptive instance: " +
createAdaptiveInstanceError.toString(),
createAdaptiveInstanceError);
}
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance. get();
// double check
if (instance == null) {
try {
// create adaptive extension
// There are two cases here: one is that there is an Adaptive class, and the other is that an Adaptive class needs to be generated
instance = createAdaptiveExtension();
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
}
}
}
}
return (T) instance;
}
getAdaptiveExtension 方法會先檢查快取,如果快取未命中,則呼叫 createAdaptiveExtension 方法建立自適應擴展。接下來,我們來看一下 createAdaptiveExtension 方法的程式碼。
private T createAdaptiveExtension() {
try {
// Get the adaptive extension class and instantiate it through reflection
return injectExtension((T) getAdaptiveExtensionClass(). newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extension ...");
}
}
createAdaptiveExtension 方法的程式碼比較少,但它包含了三個邏輯,分別是
- 呼叫 getAdaptiveExtensionClass 方法取得自適應擴展 Class 物件
- 透過反射進行實例化
- 呼叫 injectExtension 方法,為擴展實例注入依賴
前兩個邏輯比較容易理解,第三個邏輯是用於為自適應擴展物件注入依賴。這個邏輯看起來似乎是多餘的,但它是必須存在的。這裡簡單解釋一下,前面提到,Dubbo 中的自適應擴展有兩種,一種是手動編寫程式碼的,另一種是自動生成的。手動編寫程式碼的 Adaptive 擴展中可能會存在一些依賴,而自動生成的 Adaptive 擴展則不會依賴其他類。這裡呼叫 injectExtension 方法的目的就是為手動編寫程式碼的自適應擴展注入依賴,這一點需要大家注意。關於 injectExtension 方法,在前面的文章中已經分析過了,這裡就不再贅述了。接下來,分析 getAdaptiveExtensionClass 方法的邏輯。
private Class<?> getAdaptiveExtensionClass() {
// Get all extension classes through SPI
getExtensionClasses();
// Check the cache, if the cache is not empty, return the cache directly
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// Create an adaptive extension class
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
getAdaptiveExtensionClass 方法也包含了三個邏輯,分別是
- 呼叫 getExtensionClasses 方法取得所有擴展類
- 檢查快取,如果快取不為空,則返回快取
- 如果快取為空,則呼叫 createAdaptiveExtensionClass 方法建立自適應擴展類
這三個邏輯看似平平無奇,好像沒什麼好講的。但這平淡的程式碼中卻隱藏著一些細節需要說明。首先,我們從第一個邏輯入手。getExtensionClasses 方法用於獲取一個介面的所有實現類。例如,該方法可以取得 Protocol 介面的 DubboProtocol、HttpProtocol、InjvmProtocol 等實現類。在獲取實現類的過程中,如果某個實現類被 Adaptive 註解修飾,則會將該類賦值給 cachedAdaptiveClass 變數。此時,滿足上述步驟中第二步的條件(快取不為空),直接返回 cachedAdaptiveClass 即可。如果所有實現類都沒有被 Adaptive 註解修飾,則執行第三步的邏輯,創建自適應擴展類。相關程式碼如下
private Class<?> createAdaptiveExtensionClass() {
// Build adaptive extension code
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
// Get the compiler implementation class
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// Compile the code and generate Class
return compiler.compile(code, classLoader);
}
createAdaptiveExtensionClass 方法用於生成自適應擴展類。該方法首先會生成自適應擴展類的原始碼,然後通過 Compiler 實例(Dubbo 預設使用 javassist 作為編譯器)編譯原始碼,得到一個代理類 Class 實例。接下來,我們將重點關注代理類程式碼生成的邏輯,其他邏輯自行分析。
2.2.1 自適應擴展類程式碼生成
AdaptiveClassCodeGenerator#generate 方法生成擴展類程式碼
public String generate() {
// If there is no method in the interface modified by the @Adaptive annotation, an exception is thrown directly
if (!hasAdaptiveMethod()) {
throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}
StringBuilder code = new StringBuilder();
// Generate package name, import, method, etc.
code.append(generatePackageInfo());
code.append(generateImports());
code.append(generateClassDeclaration());
Method[] methods = type. getMethods();
for (Method method : methods) {
code.append(generateMethod(method));
}
code.append("}");
if (logger. isDebugEnabled()) {
logger. debug(code. toString());
}
return code. toString();
}
2.2.2 生成方法
在上面的程式碼中,生成方法的邏輯是最關鍵的,我們來詳細分析一下。
private String generateMethod(Method method) {
String methodReturnType = method. getReturnType(). getCanonicalName();
String methodName = method. getName();
// generate method content
String methodContent = generateMethodContent(method);
String methodArgs = generateMethodArguments(method);
String methodThrows = generateMethodThrows(method);
return String. format(CODE_METHOD_DECLARATION, methodReturnType, methodName, methodArgs, methodThrows, methodContent);
}
generateMethodContent 分析
private String generateMethodContent(Method method) {
// This method must be decorated with @Adaptive annotation
Adaptive adaptiveAnnotation = method. getAnnotation(Adaptive. class);
StringBuilder code = new StringBuilder(512);
if (adaptiveAnnotation == null) {
// Without @Adaptive annotation modification, exception information is generated
return generateUnsupported(method);
} else {
// Get the index of the URL on the parameter list
int urlTypeIndex = getUrlTypeIndex(method);
if (urlTypeIndex != -1) {
// Generate a null check for the URL if it exists on the parameter list
code.append(generateUrlNullCheck(urlTypeIndex));
} else {
// If there is no parameter of URL type in the parameter list, then it depends on whether the parameter object on the parameter list contains the getUrl method
// If there is, generate a URL null check
code.append(generateUrlAssignmentIndirectly(method));
}
// parse the value attribute on the Adaptive annotation
String[] value = getMethodAdaptiveValue(adaptiveAnnotation);
// If there is a parameter of type Invocation on the parameter list, generate a null check and get the methodName.
boolean hasInvocation = hasInvocationArgument(method);
code.append(generateInvocationArgumentNullCheck(method));
// This logic is mainly to generate extName (that is, the extension)
// Divided into multiple situations:
// 1. Does defaultExtName exist?
// 2. Whether there is an invocation type parameter in the parameter
// 3. Whether to generate a proxy for the protocol
// Why should the protocol be considered separately? Because there is a method to get the protocol value in the URL
code.append(generateExtNameAssignment(value, hasInvocation));
// check extName == null?
code.append(generateExtNameNullCheck(value));
// generate get extension (using ExtensionLoader.getExtension method)
code.append(generateExtensionAssignment());
// generate return statement
code.append(generateReturnAndInvocation(method));
}
return code. toString();
}
上述邏輯主要做了以下幾件事情
- 檢查方法上是否被 Adaptive 註解修飾
- 生成方法程式碼時,參數列表中必須要有 URL(或者參數物件中有 URL)
- 使用 ExtensionLoader.getExtension 獲取擴展實現
- 執行對應的方法
2.2.3 附上一段動態生成的程式碼示例
package org.apache.dubbo.common.extension.adaptive;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class HasAdaptiveExt$Adaptive implements org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt {
public java.lang.String echo(org.apache.dubbo.common.URL arg0,
java. lang. String arg1) {
// URL null check
if (arg0 == null) {
throw new IllegalArgumentException("url == null");
}
org.apache.dubbo.common.URL url = arg0;
// get the extension
String extName = url. getParameter("has. adaptive. ext", "adaptive");
// extension null check
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt) name from url (" +
url.toString() + ") use keys([has.adaptive.ext])");
}
// get extension
org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt extension = (org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.adaptive.HasAdaptiveExt.class)
.getExtension(extName);
// Execute the corresponding method
return extension.echo(arg0, arg1);
}
}
3. SPI 擴展範例
3.1 載入固定的擴展類
3.1.1 撰寫 SPI 介面及實現類
無論是 Java SPI 還是 Dubbo 中實現的 SPI,都需要撰寫一個介面。不過 Dubbo 中的介面需要使用 @SPI 註解修飾。
@SPI
public interface DemoSpi {
void say();
}
public class DemoSpiImpl implements DemoSpi {
public void say() {
}
}
3.1.2 將實現類放置在特定目錄下
從上面的程式碼中我們可以看到,dubbo 載入擴展類時,會從四個目錄中讀取。我們在 META-INF/dubbo 目錄下新建一個以 DemoSpi 介面命名的檔案,內容如下
demoSpiImpl = com.xxx.xxx.DemoSpiImpl (full class name for the implementation class of the DemoSpi interface)
3.1.3 使用
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<DemoSpi> extensionLoader =
ExtensionLoader. getExtensionLoader(DemoSpi. class);
DemoSpi dmeoSpi = extensionLoader. getExtension("demoSpiImpl");
optimusPrime. sayHello();
}
}
3.2 載入自適應擴展類
這裡以 Protocol 為例說明
3.2.1 Protocol 介面(摘取部分核心方法)
@SPI("dubbo")
public interface Protocol {
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
@Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
public class DubboProtocol extends AbstractProtocol {
…
@Override
public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
return protocolBindingRefer(type, url);
}
@Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
…
return exporter;
}
}
3.2.2 將實現類放置在特定目錄下
在 dubbo 中,配置路徑為 META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol
dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol
需要注意的是,在 dubbo 中,並不會直接使用 DubboProtocol,而是使用它的包裝類。
3.2.3 使用
public class DubboAdaptiveTest {
@Test
public void sayHello() throws Exception {
URL url = URL.valueOf("dubbo://#/test");
Protocol adaptiveProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
adaptiveProtocol. refer(type, url);
}
}