dubbo是如何實現可擴充套件的?

水木竹水發表於2022-05-26

dubbo如何實現可擴充套件的,援引官網描述

Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。

Dubbo 改進了 JDK 標準的 SPI 的以下問題:

  • JDK 標準的 SPI 會一次性例項化擴充套件點所有實現,如果有擴充套件實現初始化很耗時,但如果沒用上也載入,會很浪費資源。
  • 如果擴充套件點載入失敗,連擴充套件點的名稱都拿不到了。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取指令碼型別的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類載入失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當使用者執行 ruby 指令碼時,會報不支援 ruby,而不是真正失敗的原因。
  • 增加了對擴充套件點 IoC 和 AOP 的支援,一個擴充套件點可以直接 setter 注入其它擴充套件點。

 

定義個介面:

public interface HelloService {
    String  sayHello();
}

定義兩個實現類:

public class DogHelloService  implements HelloService {
    @Override
    public String sayHello() {
        return "wang";
    }
}
public class HumanHelloService   implements HelloService {
    @Override
    public String sayHello() {
        return "hello 你好";
    }
}

 

1.JDK標準的SPI是怎麼回事?

ServiceLoader.load方法會載入META-INF/services/目錄下定義的介面全限定名檔案,內容為實現類。

com.exm.service.impl.DogHelloService
com.exm.service.impl.HumanHelloService

 

核心為ServiceLoader.LazyIterator迭代器,在load方法被呼叫時,會初始化該迭代器,如下:

 public void reload() {
        providers.clear();
        lookupIterator = new LazyIterator(service, loader);
    }

LazyIterator會讀取配置實現類,並通過反射進行例項化(前提要求實現類需要具備無參構造)。

其中hasNextService方法會載入META-INF/services介面檔案,並載入到Enumeration<URL> configs中,原始碼如下:

private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                      //獲取配置全路徑名。如:META-INF/services/com.exm.service.HelloService
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                      //並載入到Enumeration<URL> configs中
                        configs = loader.getResources(fullName);
                } catch (IOException x) {
                    fail(service, "Error locating configuration files", x);
                }
            }
            while ((pending == null) || !pending.hasNext()) {
                if (!configs.hasMoreElements()) {
                    return false;
                }
                pending = parse(service, configs.nextElement());
            }
            nextName = pending.next();
            return true;
        }

迭代器通過nextService獲取下一個實現類物件,原始碼如下,其中包含反射拿到Class物件,並例項化。

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
              //反射例項化
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
              //轉強制化為介面,並放入LinkedHashMap<String,S> providers中
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

為什麼說JDK的SPI會一次性載入並例項化所有的擴充套件呢?

因為在調load時,實際構造了ServiceLoader.LazyIterator,如果想找到某個擴充套件實現,需要迭代器遍歷所有的實現才可以。

1⃣️如果其中有一個例項化或cast時異常,後邊所有都將無法遍歷。

2⃣️如果某個類的例項化耗時很長,並沒用到,會造成資源浪費

 

編寫一個測試方法:

public static void main(String[] args) {
        final ServiceLoader<HelloService> helloServices  = ServiceLoader.load(HelloService.class);
        for (HelloService helloService : helloServices){
            System.out.println(helloService.getClass().getName() + ":" + helloService.sayHello());
        }
    }

測試輸出:

com.exm.service.impl.DogHelloService:wang
com.exm.service.impl.HumanHelloService:hello 你好

 

2.Dubbo是怎麼進行改進的呢?

dubbo時如何進行改進的呢?

(1)dubbo定義的SPi檔案包含了key,即每個實現類對應一個不同的key,在載入class的時候,會將key和class放入一個map中。

這樣在使用者想使用哪個類的例項時,只需要例項化對應的類,無需例項化所有類

(2)Adaptive功能:實現動態的使用擴充套件點。通過 getAdaptiveExtension方法 統一對指定介面對應的所有擴充套件點進行封裝,通過URL的方式對擴充套件點來進行動態選擇。

 

2.1 載入所有擴充套件點,選擇性例項化

public class Main {
    public static void main(String[] args) {
        // 獲取擴充套件載入器
        ExtensionLoader<HelloService>  extensionLoader  = ExtensionLoader.getExtensionLoader(HelloService.class);
        // 遍歷所有的支援的擴充套件點,並將key與實現類進行關聯
        Set<String>  extensions = extensionLoader.getSupportedExtensions();
        for (String extension : extensions){
            String result = extensionLoader.getExtension(extension).sayHello();
            System.out.println(result);
        }
    }
}

ExtensionLoader.getSupportedExtensions會載入所有擴充套件類(但沒有例項化)。然後通過extensionLoader.getExtension對指定key進行例項化,這一點是與JDK不同的。

我們看下具體是怎麼載入的,getSupportedExtensions為入口,最終會通過loadDirectory進行載入

Set<String> getSupportedExtensions()
    Map<String, Class<?>> getExtensionClasses()
        Map<String, Class<?>> loadExtensionClasses()
            void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst)

(1)getSupportedExtensions方法會返回所有擴充套件點的key,供使用者使用。

public Set<String> getSupportedExtensions() {
        Map<String, Class<?>> clazzes = this.getExtensionClasses();
              //獲取到所有擴充套件點後,將key放入TreeSet中(按字串排序)
        return Collections.unmodifiableSet(new TreeSet(clazzes.keySet()));
    }

loadDirectory載入檔案的來源為以下6個部分,相容了JDK路徑。

同時載入有順序,越靠前越優先載入

private Map<String, Class<?>> loadExtensionClasses() {
        this.cacheDefaultExtensionName();
        Map<String, Class<?>> extensionClasses = new HashMap();
        this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/", this.type.getName(), true);
        this.loadDirectory(extensionClasses, "META-INF/dubbo/internal/", this.type.getName().replace("org.apache", "com.alibaba"), true);
        this.loadDirectory(extensionClasses, "META-INF/dubbo/", this.type.getName());
        this.loadDirectory(extensionClasses, "META-INF/dubbo/", this.type.getName().replace("org.apache", "com.alibaba"));
        this.loadDirectory(extensionClasses, "META-INF/services/", this.type.getName());
        this.loadDirectory(extensionClasses, "META-INF/services/", this.type.getName().replace("org.apache", "com.alibaba"));
        return extensionClasses;
    }

(2)在Set中獲取到擴點類對應的key,通過getExtension獲取對應class的例項(包含通過setter進行依賴注入)

public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        } else if ("true".equals(name)) {
            return this.getDefaultExtension();
        } else {
            Holder<Object> holder = this.getOrCreateHolder(name);
            Object instance = holder.get();
            if (instance == null) {
                synchronized(holder) {
                    instance = holder.get();
                    if (instance == null) {
                          //建立對應class的例項,完成依賴注入
                        instance = this.createExtension(name);
                        holder.set(instance);
                    }
                }
            }

            return instance;
        }
    }

createExtension方法是例項化的核心,實現了IOC和AOP,註釋如下:

private T createExtension(String name) {
         //獲取name對應的class
        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);
            }
              //依賴注入(IOC)
            injectExtension(instance);
              //包裝器(AOP)
            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);
        }
    }

 

 2.2 getAdaptiveExtension根據URL引數動態獲取相應的擴充套件點

public class AdaptiveMain {
    public static void main(String[] args) {
        URL   url  = URL.valueOf("test://localhost/hello?hello.service=dog");
        HelloService  adaptiveExtension = ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
        String  msg = adaptiveExtension.sayHello(url);
        System.out.println(msg);
    }
}

(1)核心程式碼為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;
    }

(2)建立自適應擴充套件類例項

private T createAdaptiveExtension() {
        try {
              //獲取自適應擴充套件類,並例項化,然後通過setter注入依賴
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can't create adaptive extension " + type + ", cause: " + e.getMessage(), e);
        }
    }

(3)生成自適應擴充套件類class

private Class<?> getAdaptiveExtensionClass() {
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

(4)載入類擴充套件點(與上文相同)

private Map<String, Class<?>> 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;
    }

(5)建立自適應擴充套件class,動態生成程式碼,並進行編譯

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);
}

得到如下class:

package com.exm.service;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class HelloService$Adaptive implements com.exm.service.HelloService {
public java.lang.String sayHello()  {
throw new UnsupportedOperationException("The method public abstract java.lang.String com.exm.service.HelloService.sayHello() of interface com.exm.service.HelloService is not adaptive method!");
}
public java.lang.String sayHello(org.apache.dubbo.common.URL arg0)  {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("hello.service", "human");
if(extName == null) throw new IllegalStateException("Failed to get extension (com.exm.service.HelloService) name from url (" + url.toString() + ") use keys([hello.service])");
com.exm.service.HelloService extension = (com.exm.service.HelloService)ExtensionLoader.getExtensionLoader(com.exm.service.HelloService.class).getExtension(extName);
return extension.sayHello(arg0);
}
}

可以看到自適應只支援具有adaptive註解的方法,並且引數彙總需要有URL引數。

具體邏輯,是通過獲取URL引數中的變數,ExtensionLoader.getExtensionLoader().getExtension(name)獲取具體的class例項,完成呼叫。

(6)通過setter注入依賴

private T injectExtension(T instance) {

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

    try {
        for (Method method : instance.getClass().getMethods()) {
            if (!isSetter(method)) {
                continue;
            }
            /**
             * Check {@link DisableInject} to see if we need auto injection for this property
             */
            if (method.getAnnotation(DisableInject.class) != null) {
                continue;
            }
            Class<?> pt = method.getParameterTypes()[0];
            if (ReflectUtils.isPrimitives(pt)) {
                continue;
            }

            try {
                String property = getSetterProperty(method);
                  //獲取到Adaptive物件
                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;
}

(7)上文中的objectFactory是ExtensionFactory例項,其實現類包含SpiExtensionFactory和SpringExtensionFactory,一個是dubbo的擴充套件工廠,一個是Spring的工廠;前者只支援type是SPI的介面,並生成自適應類;後者從Spring容器中獲取。

在依賴注入時,會在兩個容器中遍歷,如下:

public <T> T getExtension(Class<T> type, String name) {
    for (ExtensionFactory factory : factories) {
        T extension = factory.getExtension(type, name);
        if (extension != null) {
            return extension;
        }
    }
    return null;
}

 

(8)另外,還有一個實現類是AdaptiveExtensionFactory是預設的@Adaptive類,即被該註解修飾的類是自適應類,就不會動態生成了。

在getExtensionClasses()載入ExtensionFactory擴充套件class時,如果擴點類被Adaptive註解修飾,則將快取在ExtensionLoader.cachedAdaptiveClass中;

在getAdaptiveExtensionClass方法中,直接返回,不需要生成自適應類

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) 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.");
    }
      //判斷類事都是Adaptive類,是的話就快取,在getAdaptiveExtensionClass時直接返回
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    } else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        clazz.getConstructor();
        if (StringUtils.isEmpty(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)) {
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                cacheName(clazz, n);
                saveInExtensionClass(extensionClasses, clazz, n);
            }
        }
    }
}

 

綜上,dubbo載入class擴充套件與例項化是分開的,可以通過指定key例項化某一個class;

dubbo支援IOC和AOP;

同時,dubbo結合SPI與Adaptive註解,可以實現對所有擴充套件class封裝,然後根據URL引數動態獲取指定的class。

 

3.在注入依賴的時候是否有迴圈依賴的問題?

在dubbo建立擴充套件class例項時,會通過setter進行依賴注入,如果存在迴圈依賴,怎麼處理?

在dubbo依賴注入時,除了Spring容器外,從SPI容器中獲取,獲取的是SPI介面的自適應實現,是新建立的類,所以不存在迴圈依賴的問題。

 

牛逼的框架,就是讓你一眼看不懂它在幹什麼   ---me

相關文章