Dubbo源碼之-SPI機制

以下爲Dubbo架構圖

image-20201021163305413

SPI機制

SPI全程是Service Provider Interface,翻譯過來就是服務提供發現。通過掃描指定路徑下的配置項達到一種動態的擴展能力。

Spring-Boot的SPI機制

在Spring-Boot中,有Spring官方提供的Starter組件,也有第三方需要根據規範來實現自身的Starter組件,所以Spring官方提供了SpringFactoryLoader的SPI擴展機制。

實現Starter組件,最主要是的實現自動裝配,以減少複雜的Bean的配置。下面我們來看下自動裝配中SPI機制體現。


image-20201022101109284

Dubbo自動裝配包的META-INF路徑下,會有個spring.factories文件,可以在改文件中指定自動裝配的Bean實現。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.apache.dubbo.spring.boot.autoconfigure.DubboRelaxedBinding2AutoConfiguration

那麼大膽的猜想,在Spring-Boot容器啓動過程當中,肯定會有個東西去掃描它讓它生效。

在Spring-Boot啓動類中,會有個註解@SpringBootApplication,其內部還集成了@ EnableAutoConfiguration,import了AutoConfigurationImportSelector類來看下這個類。

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
//加載exclude include屬性(若顯式的配置了)
AnnotationAttributes attributes = getAttributes(annotationMetadata);
//加載所有META-INF/spring.factories key爲org.springframework.boot.autoconfigure.EnableAutoConfiguration的值
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
// 過濾去除當前項目無需加載的Bean
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

最後會返回一個Map<String, List<String>>結構的數據。key爲org.springframework.boot.autoconfigure.EnableAutoConfiguration,value則爲所有的路徑下的META-INF/spring.factories的指定key的值,包括Spring-Boot自身提供的以及第三方提供的都會掃描進來。

JDK的SPI機制

JDK也內置了SPI的機制實現,它的解析類是java.util.ServiceLoader,同樣他需要遵循一些標準:

  • 在classpath目錄下,創建一個目錄爲:META-INF/service

  • 該目錄下創建一個 properties 文件,該文件需要滿足以下幾個條件

    1.文件名必須是擴展的接口的全路徑名稱
    2.文件內部描述的是該擴展接口的所有實現類,多個通過換行符分隔
    3.文件的編碼格式是 UTF-8
    

ServiceLoader會去掃描META-INF/service 路徑下的properties文件。

public final class ServiceLoader<S>
    implements Iterable<S>
{

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

SPI 在很多地方有應用,可能大家都沒有關注,最常用的就是 JDBC 驅動 。來看下它是怎麼用的:

image-20201022105440812

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

這個文件裏面寫的就是 mysql 的驅動實現。我恍然大悟,原來通過 SPI 機制把java.sql.Driver和 mysql 的驅動做了集成。這樣就達到了各個數據庫廠商自己去實現數據庫連接, jdk 本身不關心你怎麼實現。

Class.forName("com.mysql.jdbc.Driver");
//直接獲的數據庫連接
Connection connection = DriverManager.getConnection(url, user, password);

DriverManager在初始化時,會去掃描所有的META-INF/services/路徑下的配置驅動,然後進行加載。

 private static void loadInitialDrivers() {
       //略
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

那麼JDK內置的SPI機制有什麼缺點呢?

  • JDK 標準的 SPI 會一次性加載實例化擴展點的所有實現,什麼意思呢?就是如果你在 META-INF/service 下的文件裏面加了 N個實現類,那麼 JDK 啓動的時候都會一次性全部加載。那麼如果有的擴展點實現初始化很耗時或者如果有些實現類並沒有用到,那麼會很浪費資源 。
  • 如果擴展點加載失敗,會導致調用方報錯,而且這個錯誤很難定位到是這個原因 。
  • 擴展如果依賴其他的擴展,做不到自動注入和裝配。
  • 不提供類似於Spring的IOC和AOP功能。

Dubbo中的SPI機制

那麼針對JDK內置的SPI機制的缺點,Dubbo進行了一定的優化。有如下兩個規則:

1.需要在 resource 目錄下配置 META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services,並基於 SPI 接口去創建一個文件
2.文件名稱和接口名稱保持一致,如org.apache.dubbo.rpc.cluster.LoadBalance,文件內容則是KEY 對應 Value。key爲別名,value爲實際的自定義的類全路徑名。

image-20201023141001713

@SPI標籤

@SPI(RandomLoadBalance.NAME)
public interface LoadBalance {

@SPI註解作用於擴展點的接口上,表明該接口是一個擴展點。可以被Dubbo的ExtentionLoader加載。如果沒有此ExtensionLoader調用會異常。Dubbo中支持擴展的接口有ConfiguratorFactoryLoadBalanceMergerProtocolClusterInterceptor等。

可以針對協議、攔截、集羣、路由、負載均衡、序列化、容器… 幾乎裏面用到的所有功能,都可以實現自己的擴展 。

@SPI註解有一個參數,該參數表示該擴展點的默認實現的別名。如果沒有顯示的指定擴展,就使用默認實現。RandomLoadBalance.NAME是一個常量,值是"random",是一個隨機負載均衡的實現。


實現一個自定義的協議擴展類

public class MyProtocol implements Protocol {

    @Override
    public int getDefaultPort() {
        return 8087;
    }

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        return null;
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        return null;
    }

    @Override
    public void destroy() {

    }
}
##在resource路徑下創建 META-INF/dubbo 名稱爲org.apache.dubbo.rpc.Protocol的文件
myProtocol=com.anto.dubbo.dubboprovider.protocol.MyProtocol
@SpringBootApplication
public class DubboProviderApplication {

    public static void main(String[] args) {
        SpringApplication.run(DubboProviderApplication.class, args);
        Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
        //能夠正確輸出8087  說明成功加載到了自定義的協議擴展類
        System.out.println(protocol.getDefaultPort());
    }

}

總的來說,思路JDK的SPI 是差不多。 都是基於約定的路徑下制定配置文件。目的是爲了通過配置的方式輕鬆實現功能的擴展。

在JDK的SPI機制當中是通過ServiceLoader來加載解析對應的文件,那麼在Dubbo中肯定也存在類似的作用的組件----EXtensionLoader

ExtensionLoader 的實現

其實可以大膽的猜想,所謂的擴展點就是指定的路徑下配置擴展接口的自定義的實現類,然後由ExtensionLoader去查找和解析這個配置文件。

ExtensionLoader 自身是一個泛型的類,也就是說每一個類型的擴展接口都有且只會實例化一個ExtensionLoader的實例。

**該方法需要一個Class類型的參數,該參數表示希望加載的擴展點類型,該參數必須是接口,且該接口必須被@SPI註解註釋,否則拒絕處理。 **

 public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type (" + type +
                    ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
        }
		//根據指定的類型查找容器中是否有此類型,無則進行創建,緩存
        ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        if (loader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
        }
        return loader;
    }

從這可以看出,對於每一個擴展,在Dubbo中只會存在一個ExtensionLoader實例。

實例化ExtensionLoader,初始化了兩個參數type,objectFactory。

 private ExtensionLoader(Class<?> type) {
        this.type = type;
     //除了ExtensionFactory,都會返回自適應的擴展點AdaptiveExtension
        objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
    }

獲取一個ExtensionLoader實例對象

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");

傳入一個別名(就是在配置文件中的key值),然後得到一個具體的ExtensionLoader的實例對象。其實根據上面的結構,大膽的猜測

1.會有個緩存機制 已經創建情況下,直接從緩存中獲取

2.不存在則進行查找創建該實例

public T getExtension(String name) {
        //...略
    	//當傳入的爲true時,取得是默認的擴展實現,即@SPI註解的值
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        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;
    }
  • createExtension
private T createExtension(String name) {
    //根據別名獲取對應的Class類型 同理,Dubbo會一次性把所有META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services的擴展接口加載後,定義成key-vaue結構
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            //緩存在ConcurrentHashMap中
            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);
    }
}

injectExtension這個方法是用來實現依賴注入的,如果被加載的實例中,有成員屬性本身也是一個擴展點,則會通過 set 方法進行注入。

若該屬性實例未被創建,則會再次觸發創建ExtensionLoader過程。


看到這,可以發現Dubbo的SPI的擴展機制相比JDk的擴展機制有了比較大的改進。

1.按需擴展 Dubbo內部通過緩存機制,只有當該擴展點被調用時纔會觸發其創建的過程
2.自動注入和裝配  若擴展類依賴其他的擴展點,可以做到自動注入

Adaptive自適應擴展點

Dubbo還支持自適應的擴展,在下面這個例子中,我們傳入一個 Compiler 接口,它會返回一個AdaptiveCompiler實例對象。這個就叫自適應。

Compiler compiler=ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
System.out.println(compiler.getClass());

自適應擴展的關鍵是一個註解 ---@Adaptive

放在類上,說明當前類是一個確定的自適應擴展點的類,則直接返回修飾的類。相當於確定這個類本身具有了自適應的功能,可以直接使用。

@Adaptive
public class AdaptiveCompiler implements Compiler {

    private static volatile String DEFAULT_COMPILER;

    public static void setDefaultCompiler(String compiler) {
        DEFAULT_COMPILER = compiler;
    }
	//本身AdaptiveCompiler這個類針對不同的場景會選擇不同的編譯解決方案,所以是一個確定的自適應擴展點的類
    @Override
    public Class<?> compile(String code, ClassLoader classLoader) {
        Compiler compiler;
        ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
        String name = DEFAULT_COMPILER; // copy reference
        if (name != null && name.length() > 0) {
            compiler = loader.getExtension(name);
        } else {
            compiler = loader.getDefaultExtension();
        }
        return compiler.compile(code, classLoader);
    }

}

如果是放在方法級別,那麼需要生成一個動態代理類,來進行轉發。

作用在類層面,整個Dubbo系統目前只有兩個AdaptiveCompiler 和AdaptiveExtensionFactory,
spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory
adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory

比如拿 Protocol 這個接口來說,它裏面定義了 export 和 refer 兩個抽象方法,這兩個方法分別帶有@Adaptive 的標識,標識是一個自適應方法。

當ExtensionLoader實例對象調用getAdaptiveExtension()方法時,判斷需要加載的的Class類型在緩存中存在與否,有則直接返回。

 public T getAdaptiveExtension() {
     //判斷對應擴展接口的ExtensionLoader對象是否持有具體的擴展接口實現類對象
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            //...

            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;
    }
  • createAdaptiveExtension()
 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);
        }
    }

    private Class<?> getAdaptiveExtensionClass() {
       //判斷是否需要加載當前擴展類的實現類(空則需要加載)
        getExtensionClasses();
         //從緩存中取 是否有@Adaptive標識的擴展接口實現類
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

cachedAdaptiveClass緩存中不爲空,則直接返回該擴展類的實現類。爲什麼呢?

 private volatile Class<?> cachedAdaptiveClass = null;

實際上,cachedAdaptiveClass是一個私有volatile變量,當第一次掃描加載ExtensionLoader實例時,會設置該值。

   
  public void addExtension(String name, Class<?> clazz) {
        getExtensionClasses(); // load classes

      //...略

        if (!clazz.isAnnotationPresent(Adaptive.class)) {
            if (StringUtils.isBlank(name)) {
                throw new IllegalStateException("Extension name is blank (Extension " + type + ")!");
            }
            if (cachedClasses.get().containsKey(name)) {
                throw new IllegalStateException("Extension name " +
                        name + " already exists (Extension " + type + ")!");
            }

            cachedNames.put(clazz, name);
            cachedClasses.get().put(name, clazz);
        } else {
          //當該實現類被@Adaptive修飾時,會設置cachedAdaptiveClass的值
            cachedAdaptiveClass = clazz;
        }
    }

如果是@Adaptive標註在方法級別呢?

動態生成字節碼,然後進行動態加載。那麼這個時候鎖返回的 Class,如果加載的是 Protocol.class,應該是 Protocol$Adaptive這個 cachedDefaultName 實際上就是擴展點接口的@SPI 註解對應的名字,如果此時加載的是 Protocol.class,那麼cachedDefaultName=dubbo 。

    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 ,會根據具體的方法傳參來決定使用哪個擴展接口實現,示意圖如下:

image-20201026100734651

Activate自動激活擴展點

自動激活擴展點有點類似Spring-Boot中的@Conditional標籤,當滿足什麼條件時,纔會加載當前Bean。

@Activate(group = {CONSUMER, PROVIDER}, value = CACHE_KEY)
public class CacheFilter implements Filter {

如上,在CacheFilter中,group 表示客戶端和和服務端都會加載, value 表示 url 中有 cache_key 的時候 ,纔會加載當前的CacheFilter對象。

    public static void main(String[] args) { 
        ExtensionLoader<Filter> loader=ExtensionLoader.getExtensionLoader(Filter.class);
        URL url=new URL("","",0);
        //當把cache寫進參數中時,會把CacheFilter加載進來
        //url=url.addParameter("cache","cache");
        List<Filter> filters=loader.getActivateExtension(url,"cache");
        System.out.println(filters.size());//結果分別是10 或者11
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章