天天看點

Dubbo 擴充點加載機制:從 Java SPI 到 Dubbo SPI

Dubbo 擴充點加載機制:從 Java SPI 到 Dubbo SPI

SPI 全稱為 Service Provider Interface,是一種服務發現機制。當程式運作調用接口時,會根據配置檔案或預設規則資訊加載對應的實作類。是以在程式中并沒有直接指定使用接口的哪個實作,而是在外部進行裝配。

要想了解 Dubbo 的設計與實作,其中 Dubbo SPI 加載機制是必須了解的,在 Dubbo 中有大量功能的實作都是基于 Dubbo SPI 實作解耦,同時也使得 Dubbo 獲得如此好的可擴充性。

Java SPI

通過完成一個 Java SPI 的操作來了解它的機制。

建立一個 AnimalService 接口及 category 方法

建立一個實作類 Cat

建立 META-INF/services 目錄,并在該目錄下建立一個檔案,檔案名為 AnimalService 的全限定名作為檔案名

在檔案中添加實作類 Cat 的全限定名

Animal 接口

public interface AnimalService {

void category();           

}

Cat 實作類

public class Cat implements AnimalService {

@Override
public void category() {
    System.out.println("cat: Meow ~");
}           

在 META-INF/services 目錄下的 top.ytao.demo.spi.AnimalService 檔案中添加:

top.ytao.demo.spi.Cat

加載 SPI 的實作:

public class JavaSPITest {

@Test
public void javaSPI() throws Exception {
    ServiceLoader<AnimalService> serviceLoader = ServiceLoader.load(AnimalService.class);
    // 周遊在配置檔案中已配置的 AnimalService 的所有實作類
    for (AnimalService animalService : serviceLoader) {
        animalService.category();
    }
}
           

執行結果:

就這樣,一個 Java SPI 就實作完成了,通過 ServiceLoader.load 擷取加載所有接口已配置的接口實作類,然後可以周遊找出需要的實作。

Dubbo SPI

本文 Dubbo 版本為2.7.5

Dubbo SPI 相較于 Java SPI 更為強大,并且都是由自己實作的一套 SPI 機制。其中主要的改進和優化:

相對于 Java SPI 一次性加載所有實作,Dubbo SPI 是按需加載,隻加載需要使用的實作類。同時帶有緩存支援。

更為詳細的擴充加載失敗資訊。

增加了對擴充 IOC 和 AOP的支援。

Dubbo SPI 示例

Dubbo SPI 的配置檔案放在 META-INF/dubbo 下面,并且實作類的配置方式采用 K-V 的方式,key 為執行個體化對象傳入的參數,value 為擴充點實作類全限定名。例如 Cat 的配置檔案内容:

cat = top.ytao.demo.spi.Cat

Dubbo SPI 加載過程中,對 Java SPI 的目錄也是可以被相容的。

同時需要在接口上增加 @SPI 注解,@SPI 中可以指定 key 值,加載 SPI 如下:

public class DubboSPITest {

@Test
public void dubboSPI(){
    ExtensionLoader<AnimalService> extensionLoader = ExtensionLoader.getExtensionLoader(AnimalService.class);
    // 擷取擴充類實作
    AnimalService cat = extensionLoader.getExtension("cat");
    System.out.println("Dubbo SPI");
    cat.category();
}
           

執行結果如下:

擷取 ExtensionLoader 執行個體

擷取 ExtensionLoader 執行個體是通過上面 getExtensionLoader 方法,具體實作代碼:

public static ExtensionLoader getExtensionLoader(Class type) {

if (type == null) {
    throw new IllegalArgumentException("Extension type == null");
}
// 檢查 type 必須為接口
if (!type.isInterface()) {
    throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
// 檢查接口是否有 SPI 注解
if (!withExtensionAnnotation(type)) {
    throw new IllegalArgumentException("Extension type (" + type +
            ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// 緩存中擷取 ExtensionLoader 執行個體
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
    // 加載 ExtensionLoader 執行個體到緩存中
    EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
    loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;           

上面擷取擴充類加載器過程主要是檢查傳入的 type 是否合法,以及從擴充類加載器緩存中是否存在目前類型的接口,如果不存在則添加目前接口至緩存中。

ConcurrentMap, ExtensionLoader<?>> EXTENSION_LOADERS 是擴充類加載器的緩存,它是以接口作為 key, 擴充類加載器作為 value 進行緩存。

擷取擴充類對象

擷取擴充類對象的方法ExtensionLoader#getExtension,在這裡完成擴充對象的緩存及建立工作:

public T getExtension(String name) {

if (StringUtils.isEmpty(name)) {
    throw new IllegalArgumentException("Extension name == null");
}
// 如果傳入的參數為 true ,則擷取預設擴充類對象操作
if ("true".equals(name)) {
    return getDefaultExtension();
}
// 擷取擴充對象,Holder 裡的 value 屬性儲存着擴充對象執行個體
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
// 使用雙重檢查鎖
if (instance == null) {
    synchronized (holder) {
        instance = holder.get();
        if (instance == null) {
            // 建立擴充對象
            instance = createExtension(name);
            holder.set(instance);
        }
    }
}
return (T) instance;           

擷取 holder 對象是從緩存ConcurrentMap> cachedInstances中擷取,如果不存在,則以擴充名 key,建立一個 Holder 對象作為 value,設定到擴充對象緩存。

如果是新建立的擴充對象執行個體,那麼 holder.get() 一定是 null ,擴充對象為空時,經過雙重檢查鎖,建立擴充對象。

建立擴充對象

建立擴充對象過程:

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 (CollectionUtils.isNotEmpty(wrapperClasses)) {
        for (Class<?> wrapperClass : wrapperClasses) {
            // 建立包裝擴充類執行個體,并向其注入依賴
            instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
        }
    }
    // 初始化擴充對象
    initExtension(instance);
    return instance;
} catch (Throwable t) {
    throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
            type + ") couldn't be instantiated: " + t.getMessage(), t);
}           

上面建立擴充過程中,裡面有個 Wrapper 類,這裡使用到裝飾器模式,該類是沒有具體的實作,而是把通用邏輯進行抽象。

建立這個過程是從所有擴充類中擷取目前擴充名對應映射關系的擴充類,以及向目前擴充對象注入依賴。

擷取所有擴充類:

private Map> getExtensionClasses() {

// 擷取普通擴充類緩存
Map<String, Class<?>> classes = cachedClasses.get();
// 如果緩存中沒有,通過雙重檢查鎖後進行加載
if (classes == null) {
    synchronized (cachedClasses) {
        classes = cachedClasses.get();
        if (classes == null) {
            // 加載全部擴充類
            classes = loadExtensionClasses();
            cachedClasses.set(classes);
        }
    }
}
return classes;           

檢查普通擴充類緩存是否為空,如果不為空則重新加載,真正加載擴充類在loadExtensionClasses中:

private static final String SERVICES_DIRECTORY = "META-INF/services/";

private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

private Map> loadExtensionClasses() {

// 擷取 @SPI 上的預設擴充名
cacheDefaultExtensionName();

Map<String, Class<?>> extensionClasses = new HashMap<>();
// 先加載 Dubbo 内部的擴充類, 通過 Boolean 值控制
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName(), true);
// 由于 Dubbo 遷到 apache ,是以包名有變化,會替換之前的 alibaba 為 apache
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"), true);

loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;           

上面擷取 @SPI 擴充名,以及指定要加載的檔案。從上面靜态常量中,我們可以看到,Dubbo SPI 也是支援加載 Java SPI 的目錄,同時還加載 META-INF/dubbo/internal (該目錄為 Dubbo 的内部擴充類目錄),在 loadDirectory 加載目錄配置檔案。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst) {
    // 擷取檔案在項目中的路徑,如:META-INF/dubbo/top.ytao.demo.spi.AnimalService
    String fileName = dir + type;
    try {
        Enumeration<java.net.URL> urls = null;
        ClassLoader classLoader = findClassLoader();
        
        // 加載内部擴充類
        if (extensionLoaderClassLoaderFirst) {
            ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
            if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
                urls = extensionLoaderClassLoader.getResources(fileName);
            }
        }
        
        // 加載目前 fileName 檔案
        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();
                // 加載檔案内容
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception occurred when loading extension class (interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}           

這裡擷取檔案名後加載所有同名檔案,然後疊代各個檔案,逐個加載檔案内容。

private void loadResource(Map> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {

try {
    try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
        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('=');
                    // 如果目前行存在 "=",将 "=" 左右的值分開複制給 name 和 line
                    if (i > 0) {
                        name = line.substring(0, i).trim();
                        line = line.substring(i + 1).trim();
                    }
                    if (line.length() > 0) {
                        // 加載擴充類
                        loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                    }
                } 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);
}           

上面代碼完成檔案内容加載和解析,接下來通過 loadClass 加載擴充類。

private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {

// 檢查目前實作類是否實作了 type 接口
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.");
}

// 目前實作類是否有 Adaptive 注解
if (clazz.isAnnotationPresent(Adaptive.class)) {
    cacheAdaptiveClass(clazz);
// 目前類是否為 Wrapper 包裝擴充類 
} else if (isWrapperClass(clazz)) {
    cacheWrapperClass(clazz);
} else {
    // 嘗試目前類是否有無參構造方法
    clazz.getConstructor();
    
    if (StringUtils.isEmpty(name)) {
        // 如果 name 為空,則擷取 clazz 的 @Extension 注解的值,如果注解值也沒有,則使用小寫類名
        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)) {
        // 緩存 擴充名和@Activate的緩存
        cacheActivateClass(clazz, names[0]);
        for (String n : names) {
            // 緩存 擴充類和擴充名的緩存
            cacheName(clazz, n);
            // 将 擴充類和擴充名 儲存到extensionClasses 擴充名->擴充類 關系映射中
            saveInExtensionClass(extensionClasses, clazz, n);
        }
    }
}           

至此,getExtensionClasses() 加載擴充類方法分析完成,接下分析注入依賴 injectExtension() 方法。

private T injectExtension(T instance) {

// 
if (objectFactory == null) {
    return instance;
}

try {
    for (Method method : instance.getClass().getMethods()) {
        // 周遊目前擴充類的全部方法,如果目前方法不屬于 setter 方法,
        // 即不是以 'set'開頭的方法名,參數不是一個的,該方法通路級别不是 public 的,則不往下執行
        if (!isSetter(method)) {
            continue;
        }
        
        // 目前方法是否添加了不要注入依賴的注解
        if (method.getAnnotation(DisableInject.class) != null) {
            continue;
        }
        Class<?> pt = method.getParameterTypes()[0];
        // 判斷目前參數是否屬于 八個基本類型或void
        if (ReflectUtils.isPrimitives(pt)) {
            continue;
        }

        try {
            // 通過屬性 setter 方法擷取屬性名
            String property = getSetterProperty(method);
            // 擷取依賴對象
            Object object = objectFactory.getExtension(pt, property);
            if (object != null) {
                // 設定依賴
                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;           

通過周遊擴充類所有方法,找到相對應的依賴,然後使用反射調用 settter 方法來進行設定依賴。

objectFactory 對象如圖:

其中找到相應依賴是在 SpiExtensionFactory 或 SpringExtensionFactory 中,同時,這兩個 Factory 儲存在 AdaptiveExtensionFactory 中進行維護。

@Adaptive

public class AdaptiveExtensionFactory implements ExtensionFactory {

private final List<ExtensionFactory> factories;

public AdaptiveExtensionFactory() {
    // ......
}

@Override
public <T> T getExtension(Class<T> type, String name) {
    // 通過周遊比對到 type->name 的映射
    for (ExtensionFactory factory : factories) {
        T extension = factory.getExtension(type, name);
        if (extension != null) {
            return extension;
        }
    }
    return null;
}
           

以上是對 Dubbo SPI 擴充類簡單加載過程分析完成。

自适應加載機制

為 Dubbo 更加靈活的使一個接口不通過寫死加載擴充機制,而是通過使用過程中進行加載,Dubbo 的另一加載機制——自适應加載。

自适應加載機制使用 @Adaptive 标注:

@Documented

@Retention(RetentionPolicy.RUNTIME)

@Target({ElementType.TYPE, ElementType.METHOD})

public @interface Adaptive {

String[] value() default {};           

Adaptive 的值是一個數組,可以配置多個 key。初始化時,周遊所有 key 進行比對,如果沒有則比對 @SPI 的值。

當 Adaptive 注解标注在類上時,則簡單對應該實作。如果注解标注在接口方法上時,則會根據參數動态生成代碼來擷取擴充點的實作。

類上注解處理還是比較好了解,方法上的注解加載相對比較有研讀性。通過調用ExtensionLoader#getAdaptiveExtension來進行擷取擴充實作。

public T getAdaptiveExtension() {

// 擷取執行個體化對象緩存
Object instance = cachedAdaptiveInstance.get();
if (instance == null) {
    if (createAdaptiveInstanceError != null) {
        throw new IllegalStateException("Failed to create adaptive instance: " +
                createAdaptiveInstanceError.toString(),
                createAdaptiveInstanceError);
    }
    // 雙重檢查鎖後建立自适應擴充
    synchronized (cachedAdaptiveInstance) {
        instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            try {
                // 建立自适應擴充
                instance = createAdaptiveExtension();
                cachedAdaptiveInstance.set(instance);
            } catch (Throwable t) {
                createAdaptiveInstanceError = t;
                throw new IllegalStateException("Failed to create adaptive instance: " + t.toString(), t);
            }
        }
    }
}

return (T) instance;           

private T createAdaptiveExtension() {

try {
    // 擷取自适應擴充後,注入依賴
    return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
    throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
}           

上面代碼完成了擴充類對象是否存在緩存中,如果不存在,則通過建立自适應擴充,并将執行個體注入依賴後,設定在執行個體化後的自适應擴充對象中。

其中getAdaptiveExtensionClass是比較核心的流程。

private Class<?> getAdaptiveExtensionClass() {

// 加載全部擴充類
getExtensionClasses();
// 加載全部擴充類後,如果有 @Adaptive 标注的類,cachedAdaptiveClass 則一定不會為空
if (cachedAdaptiveClass != null) {
    return cachedAdaptiveClass;
}
// 建立自适應擴充類
return cachedAdaptiveClass = createAdaptiveExtensionClass();           

private Class<?> createAdaptiveExtensionClass() {

// 生成自适應擴充代碼
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
// 擷取擴充類加載器
ClassLoader classLoader = findClassLoader();
// 擷取編譯器類型的實作類
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
// 編譯代碼,傳回該對象
return compiler.compile(code, classLoader);           

這裡完成的工作主要是,加載全部擴充類,代表所有擴充接口類的實作類,在其加載過程中,如果有 @Adaptive 标注的類,會儲存到 cachedAdaptiveClass 中。通過自動生成自适應擴充代碼,并被編譯後,擷取擴充類執行個體化對象。

上面編譯器類型是可以指定的,通過 compiler 進行指定,例如:,該編譯器預設使用 javassist 編譯器。

在 generate 方法中動态生成代碼:

public String generate() {

// 檢查目前擴充接口的方法上是否有 Adaptive 注解
if (!hasAdaptiveMethod()) {
    throw new IllegalStateException("No adaptive method exist on extension " + type.getName() + ", refuse to create the adaptive class!");
}

// 生成代碼
StringBuilder code = new StringBuilder();
// 生成類的包名
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();           

上面是生成類資訊的方法,生成設計原理是按照已設定好的模闆,進行替換操作,生成類。具體資訊不代碼很多,但閱讀還是比較簡單。

自适應加載機制,已簡單分析完,咋一眼看,非常複雜,但是了解整體結構和流程,再去細研的話,相對還是好了解。

總結

從 Dubbo 設計來看,其良好的擴充性,比較重要的一點是得益于 Dubbo SPI 加載機制。在學習它的設計理念,對可擴充性方面的編碼思考也有一定的啟發。

原文位址

https://www.cnblogs.com/ytao-blog/p/12580462.html