新聞中心
[[357484]]

創(chuàng)新互聯(lián)是一家專業(yè)提供大安市企業(yè)網(wǎng)站建設(shè),專注與成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)、H5高端網(wǎng)站建設(shè)、小程序制作等業(yè)務(wù)。10年已為大安市眾多企業(yè)、政府機(jī)構(gòu)等服務(wù)。創(chuàng)新互聯(lián)專業(yè)的建站公司優(yōu)惠進(jìn)行中。
SPI是什么
SPI是一種簡(jiǎn)稱,全名叫 Service Provider Interface,Java本身提供了一套SPI機(jī)制,SPI 的本質(zhì)是將接口實(shí)現(xiàn)類的全限定名配置在文件中,并由服務(wù)加載器讀取配置文件,加載實(shí)現(xiàn)類,這樣可以在運(yùn)行時(shí),動(dòng)態(tài)為接口替換實(shí)現(xiàn)類,這也是很多框架組件實(shí)現(xiàn)擴(kuò)展功能的一種手段。
而今天要說(shuō)的Dubbo SPI機(jī)制和Java SPI還是有一點(diǎn)區(qū)別的,Dubbo 并未使用 Java 原生的 SPI 機(jī)制,而是對(duì)他進(jìn)行了改進(jìn)增強(qiáng),進(jìn)而可以很容易地對(duì)Dubbo進(jìn)行功能上的擴(kuò)展。
學(xué)東西得帶著問(wèn)題去學(xué),我們先提幾個(gè)問(wèn)題,再接著看
1.什么是SPI(開頭已經(jīng)解釋了)
2.Dubbo SPI和Java原生的有什么區(qū)別
3.兩種實(shí)現(xiàn)應(yīng)該如何寫出來(lái)
Java SPI是如何實(shí)現(xiàn)的
先定義一個(gè)接口:
- public interface Car {
- void startUp();
- }
然后創(chuàng)建兩個(gè)類,都實(shí)現(xiàn)這個(gè)Car接口
- public class Truck implements Car{
- @Override
- public void startUp() {
- System.out.println("The truck started");
- }
- }
- public class Train implements Car{
- @Override
- public void startUp() {
- System.out.println("The train started");
- }
- }
然后在項(xiàng)目META-INF/services文件夾下創(chuàng)建一個(gè)名稱為接口的全限定名,com.example.demo.spi.Car。
文件內(nèi)容寫上實(shí)現(xiàn)類的全限定名,如下:
- com.example.demo.spi.Train
- com.example.demo.spi.Truck
最后寫一個(gè)測(cè)試代碼:
- public class JavaSPITest {
- @Test
- public void testCar() {
- ServiceLoader
serviceLoader = ServiceLoader.load(Car.class); - serviceLoader.forEach(Car::startUp);
- }
- }
執(zhí)行完的輸出結(jié)果:
- The train started
- The truck started
Dubbo SPI是如何實(shí)現(xiàn)的
Dubbo 使用的SPI并不是Java原生的,而是重新實(shí)現(xiàn)了一套,其主要邏輯都在ExtensionLoader類中,邏輯也不難,后面會(huì)稍帶講一下
看看使用,和Java的差不了太多,基于前面的例子來(lái)看下,接口類需要加上@SPI注解:
- @SPI
- public interface Car {
- void startUp();
- }
實(shí)現(xiàn)類不需要改動(dòng)
配置文件需要放在META-INF/dubbo下面,配置寫法有些區(qū)別,直接看代碼:
- train = com.example.demo.spi.Train
- truck = com.example.demo.spi.Truck
最后就是測(cè)試類了,先看代碼:
- public class JavaSPITest {
- @Test
- public void testCar() {
- ExtensionLoader
extensionLoader = ExtensionLoader.getExtensionLoader(Car.class); - Car car = extensionLoader.getExtension("train");
- car.startUp();
- }
- }
執(zhí)行結(jié)果:
- The train started
Dubbo SPI中常用的注解
- @SPI 標(biāo)記為擴(kuò)展接口
- @Adaptive自適應(yīng)拓展實(shí)現(xiàn)類標(biāo)志
- @Activate 自動(dòng)激活條件的標(biāo)記
總結(jié)一下兩者區(qū)別:
- 使用上的區(qū)別Dubbo使用ExtensionLoader而不是ServiceLoader了,其主要邏輯都封裝在這個(gè)類中
- 配置文件存放目錄不一樣,Java的在META-INF/services,Dubbo在META-INF/dubbo,META-INF/dubbo/internal
- Java SPI 會(huì)一次性實(shí)例化擴(kuò)展點(diǎn)所有實(shí)現(xiàn),如果有擴(kuò)展實(shí)現(xiàn)初始化很耗時(shí),并且又用不上,會(huì)造成大量資源被浪費(fèi)
- Dubbo SPI 增加了對(duì)擴(kuò)展點(diǎn) IOC 和 AOP 的支持,一個(gè)擴(kuò)展點(diǎn)可以直接 setter 注入其它擴(kuò)展點(diǎn)
- Java SPI加載過(guò)程失敗,擴(kuò)展點(diǎn)的名稱是拿不到的。比如:JDK 標(biāo)準(zhǔn)的 ScriptEngine,getName() 獲取腳本類型的名稱,如果 RubyScriptEngine 因?yàn)樗蕾嚨?jruby.jar 不存在,導(dǎo)致 RubyScriptEngine 類加載失敗,這個(gè)失敗原因是不會(huì)有任何提示的,當(dāng)用戶執(zhí)行 ruby 腳本時(shí),會(huì)報(bào)不支持 ruby,而不是真正失敗的原因
前面的3個(gè)問(wèn)題是不是已經(jīng)能回答出來(lái)了?是不是非常簡(jiǎn)單
Dubbo SPI源碼分析
Dubbo SPI使用上是通過(guò)ExtensionLoader的getExtensionLoader方法獲取一個(gè) ExtensionLoader 實(shí)例,然后再通過(guò) ExtensionLoader 的 getExtension 方法獲取拓展類對(duì)象。這其中,getExtensionLoader 方法用于從緩存中獲取與拓展類對(duì)應(yīng)的 ExtensionLoader,如果沒(méi)有緩存,則創(chuàng)建一個(gè)新的實(shí)例,直接上代碼:
- public T getExtension(String name) {
- if (name == null || name.length() == 0) {
- throw new IllegalArgumentException("Extension name == null");
- }
- if ("true".equals(name)) {
- // 獲取默認(rèn)的拓展實(shí)現(xiàn)類
- return getDefaultExtension();
- }
- // 用于持有目標(biāo)對(duì)象
- Holder
- if (holder == null) {
- cachedInstances.putIfAbsent(name, new Holder
- holder = cachedInstances.get(name);
- }
- Object instance = holder.get();
- // DCL
- if (instance == null) {
- synchronized (holder) {
- instance = holder.get();
- if (instance == null) {
- // 創(chuàng)建擴(kuò)展實(shí)例
- instance = createExtension(name);
- // 設(shè)置實(shí)例到 holder 中
- holder.set(instance);
- }
- }
- }
- return (T) instance;
- }
上面這一段代碼主要做的事情就是先檢查緩存,緩存不存在創(chuàng)建擴(kuò)展對(duì)象
接下來(lái)我們看看創(chuàng)建的過(guò)程:
- private T createExtension(String name) {
- // 從配置文件中加載所有的擴(kuò)展類,可得到“配置項(xiàng)名稱”到“配置類”的映射關(guān)系表
- Class> clazz = getExtensionClasses().get(name);
- if (clazz == null) {
- throw findException(name);
- }
- try {
- T instance = (T) EXTENSION_INSTANCES.get(clazz);
- if (instance == null) {
- // 反射創(chuàng)建實(shí)例
- EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
- instance = (T) EXTENSION_INSTANCES.get(clazz);
- }
- // 向?qū)嵗凶⑷胍蕾?nbsp;
- injectExtension(instance);
- Set
> wrapperClasses = cachedWrapperClasses; - if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
- // 循環(huán)創(chuàng)建 Wrapper 實(shí)例
- for (Class> wrapperClass : wrapperClasses) {
- // 將當(dāng)前 instance 作為參數(shù)傳給 Wrapper 的構(gòu)造方法,并通過(guò)反射創(chuàng)建 Wrapper 實(shí)例。
- // 然后向 Wrapper 實(shí)例中注入依賴,最后將 Wrapper 實(shí)例再次賦值給 instance 變量
- instance = injectExtension(
- (T) wrapperClass.getConstructor(type).newInstance(instance));
- }
- }
- return instance;
- } catch (Throwable t) {
- throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
- type + ") couldn't be instantiated: " + t.getMessage(), t);
- }
- }
這段代碼看著繁瑣,其實(shí)也不難,一共只做了4件事情:
1.通過(guò)getExtensionClasses獲取所有配置擴(kuò)展類
2.反射創(chuàng)建對(duì)象
3.給擴(kuò)展類注入依賴
4.將擴(kuò)展類對(duì)象包裹在對(duì)應(yīng)的Wrapper對(duì)象里面
我們?cè)谕ㄟ^(guò)名稱獲取擴(kuò)展類之前,首先需要根據(jù)配置文件解析出擴(kuò)展類名稱到擴(kuò)展類的映射關(guān)系表,之后再根據(jù)擴(kuò)展項(xiàng)名稱從映射關(guān)系表中取出相應(yīng)的拓展類即可。相關(guān)過(guò)程的代碼如下:
- private Map
> getExtensionClasses() { - // 從緩存中獲取已加載的拓展類
- Map
> classes = cachedClasses.get(); - // DCL
- if (classes == null) {
- synchronized (cachedClasses) {
- classes = cachedClasses.get();
- if (classes == null) {
- // 加載擴(kuò)展類
- classes = loadExtensionClasses();
- cachedClasses.set(classes);
- }
- }
- }
- return classes;
- }
這里也是先檢查緩存,若緩存沒(méi)有,則通過(guò)一次雙重鎖檢查緩存,判空。此時(shí)如果 classes 仍為 null,則通過(guò) loadExtensionClasses 加載拓展類。下面是 loadExtensionClasses 方法的代碼
- private Map
> loadExtensionClasses() { - // 獲取 SPI 注解,這里的 type 變量是在調(diào)用 getExtensionLoader 方法時(shí)傳入的
- final SPI defaultAnnotation = type.getAnnotation(SPI.class);
- if (defaultAnnotation != null) {
- String value = defaultAnnotation.value();
- if ((value = value.trim()).length() > 0) {
- // 對(duì) SPI 注解內(nèi)容進(jìn)行切分
- String[] names = NAME_SEPARATOR.split(value);
- // 檢測(cè) SPI 注解內(nèi)容是否合法,不合法則拋出異常
- if (names.length > 1) {
- throw new IllegalStateException("more than 1 default extension name on extension...");
- }
- // 設(shè)置默認(rèn)名稱,參考 getDefaultExtension 方法
- if (names.length == 1) {
- cachedDefaultName = names[0];
- }
- }
- }
- Map
> extensionClasses = new HashMap >(); - // 加載指定文件夾下的配置文件
- loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
- loadDirectory(extensionClasses, DUBBO_DIRECTORY);
- loadDirectory(extensionClasses, SERVICES_DIRECTORY);
- return extensionClasses;
- }
loadExtensionClasses 方法總共做了兩件事情,一是對(duì) SPI 注解進(jìn)行解析,二是調(diào)用 loadDirectory 方法加載指定文件夾配置文件。SPI 注解解析過(guò)程比較簡(jiǎn)單,無(wú)需多說(shuō)。下面我們來(lái)看一下 loadDirectory 做了哪些事情
- private void loadDirectory(Map
> extensionClasses, String dir) { - // fileName = 文件夾路徑 + type 全限定名
- String fileName = dir + type.getName();
- try {
- Enumeration
urls; - ClassLoader classLoader = findClassLoader();
- // 根據(jù)文件名加載所有的同名文件
- 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);
- }
- }
loadDirectory 方法先通過(guò) classLoader 獲取所有資源鏈接,然后再通過(guò) loadResource 方法加載資源。我們繼續(xù)跟下去,看一下 loadResource 方法的實(shí)現(xiàn)
- private void loadResource(Map
> extensionClasses, ClassLoader classLoader, - java.net.URL resourceURL) {
- try {
- BufferedReader reader = new BufferedReader(
- new InputStreamReader(resourceURL.openStream(), "utf-8"));
- try {
- String line;
- // 按行讀取配置內(nèi)容
- while ((line = reader.readLine()) != null) {
- // 定位 # 字符
- final int ci = line.indexOf('#');
- if (ci >= 0) {
- // 截取 # 之前的字符串,# 之后的內(nèi)容為注釋,需要忽略
- line = line.substring(0, ci);
- }
- line = line.trim();
- if (line.length() > 0) {
- try {
- String name = null;
- int i = line.indexOf('=');
- if (i > 0) {
- // 以等于號(hào) = 為界,截取鍵與值
- name = line.substring(0, i).trim();
- line = line.substring(i + 1).trim();
- }
- if (line.length() > 0) {
- // 加載類,并通過(guò) loadClass 方法對(duì)類進(jìn)行緩存
- loadClass(extensionClasses, resourceURL,
- Class.forName(line, true, classLoader), name);
- }
- } catch (Throwable t) {
- IllegalStateException e =
- new IllegalStateException("Failed to load extension class...");
- }
- }
- }
- } finally {
- reader.close();
- }
- } catch (Throwable t) {
- logger.error("Exception when load extension class...");
- }
- }
loadResource 方法用于讀取和解析配置文件,并通過(guò)反射加載類,最后調(diào)用 loadClass 方法進(jìn)行其他操作。loadClass 方法用于主要用于操作緩存,該方法的邏輯如下:
- private void loadClass(Map
> extensionClasses, java.net.URL resourceURL, - Class> clazz, String name) throws NoSuchMethodException {
- if (!type.isAssignableFrom(clazz)) {
- throw new IllegalStateException("...");
- }
- // 檢測(cè)目標(biāo)類上是否有 Adaptive 注解
- if (clazz.isAnnotationPresent(Adaptive.class)) {
- if (cachedAdaptiveClass == null) {
- // 設(shè)置 cachedAdaptiveClass緩存
- cachedAdaptiveClass = clazz;
- } else if (!cachedAdaptiveClass.equals(clazz)) {
- throw new IllegalStateException("...");
- }
- // 檢測(cè) clazz 是否是 Wrapper 類型
- } else if (isWrapperClass(clazz)) {
- Set
> wrappers = cachedWrapperClasses; - if (wrappers == null) {
- cachedWrapperClasses = new ConcurrentHashSet
>(); - wrappers = cachedWrapperClasses;
- }
- // 存儲(chǔ) clazz 到 cachedWrapperClasses 緩存中
- wrappers.add(clazz);
- // 程序進(jìn)入此分支,表明 clazz 是一個(gè)普通的拓展類
- } else {
- // 檢測(cè) clazz 是否有默認(rèn)的構(gòu)造方法,如果沒(méi)有,則拋出異常
- clazz.getConstructor();
- if (name == null || name.length() == 0) {
- // 如果 name 為空,則嘗試從 Extension 注解中獲取 name,或使用小寫的類名作為 name
- name = findAnnotationName(clazz);
- if (name.length() == 0) {
- throw new IllegalStateException("...");
- }
- }
- // 切分 name
- String[] names = NAME_SEPARATOR.split(name);
- if (names != null && names.length > 0) {
- Activate activate = clazz.getAnnotation(Activate.class);
- if (activate != null) {
- // 如果類上有 Activate 注解,則使用 names 數(shù)組的第一個(gè)元素作為鍵,
- // 存儲(chǔ) name 到 Activate 注解對(duì)象的映射關(guān)系
- cachedActivates.put(names[0], activate);
- }
- for (String n : names) {
- if (!cachedNames.containsKey(clazz)) {
- // 存儲(chǔ) Class 到名稱的映射關(guān)系
- cachedNames.put(clazz, n);
- }
- Class> c = extensionClasses.get(n);
- if (c == null) {
- // 存儲(chǔ)名稱到 Class 的映射關(guān)系
- extensionClasses.put(n, clazz);
- } else if (c != clazz) {
- throw new IllegalStateException("...");
- }
- }
- }
- }
- }
綜上,loadClass方法操作了不同的緩存,比如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等等
到這里基本上關(guān)于緩存類加載的過(guò)程就分析完了,其他邏輯不難,認(rèn)真地讀下來(lái)加上Debug一下都能看懂的。
總結(jié)
從設(shè)計(jì)思想上來(lái)看的話,SPI是對(duì)迪米特法則和開閉原則的一種實(shí)現(xiàn)。
開閉原則:對(duì)修改關(guān)閉對(duì)擴(kuò)展開放。這個(gè)原則在眾多開源框架中都非常常見,Spring的IOC容器也是大量使用。
迪米特法則:也叫最小知識(shí)原則,可以解釋為,不該直接依賴關(guān)系的類之間,不要依賴;有依賴關(guān)系的類之間,盡量只依賴必要的接口。
那Dubbo的SPI為什么不直接使用Spring的呢,這一點(diǎn)從眾多開源框架中也許都能窺探一點(diǎn)端倪出來(lái),因?yàn)楸旧碜鳛殚_源框架是要融入其他框架或者一起運(yùn)行的,不能作為依賴被依賴對(duì)象存在。
再者對(duì)于Dubbo來(lái)說(shuō),直接用Spring IOC AOP的話有一些架構(gòu)臃腫,完全沒(méi)必要,所以自己實(shí)現(xiàn)一套輕量級(jí)反而是最優(yōu)解
本文轉(zhuǎn)載自微信公眾號(hào)「 架構(gòu)技術(shù)專欄」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系 架構(gòu)技術(shù)專欄公眾號(hào)。
分享標(biāo)題:面試重點(diǎn):來(lái)說(shuō)說(shuō)DubboSPI機(jī)制
路徑分享:http://m.fisionsoft.com.cn/article/dhhhgpg.html


咨詢
建站咨詢
