dubbo原理:SPI机制(一)

JDK SPI 和Dubbo SPI的区别:

  • JDK SPI 会一次性实例化所有配置的实例:如果某些实例在程序中并不需要,那将会是极大的浪费。Dubbo SPI只会实例化需要的类。
  • Dubbo SPI 还支持IOC(注入其他的实例)
  • Dubbo SPI的灵活性更强,功能也更加强大。

Dubbo SPI 将分成两篇来研究:

  1. dubbo原理:SPI机制(一): 主要研究dubbo SPI的原理。
  2. dubbo原理:SPI机制(二),主要研究dubbo IOC的原理

研究dubbo SPI机制时,文章均从dubbo的测试类开始,这样让读者更容易理解dubbo SPI的实际用途。

1. 起点:测试类

SimpleExt
@SPI("impl1")
public interface SimpleExt {
    // @Adaptive example, do not specify a explicit key.
    @Adaptive
    String echo(URL url, String s);
    @Adaptive({"key1", "key2"})
    String yell(URL url, String s);
    // no @Adaptive
    String bang(URL url, int i);
}

接口SimpleExt有三个实现类;并且SimpleExt添加类SPI注解,定义类两个Adaptive方法。
我们先来看下面这段测试代码:

SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map<String, String> map = new HashMap<String, String>();
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl1-echo", echo);

2. 获取ExtensionLoader

首先会获取测试类的ExtensionLoader:每个类都会有一个ExtensionLoader,自适应类就是在ExtensionLoader中产生的。
a. getExtensionLoader

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type){

    //首先会校验传入的type:是否为空、是否是interface、是否使用类SPI注解。
    ... 
    // 尝试从缓存中获取ExtensionLoader,每个type都有一个ExtensionLoader
    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;
}

b. ExtensionLoader 构造函数

private ExtensionLoader(Class<?> type) {
    this.type = type;
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

ExtensionLoader的构造函数,其实就是采用SPI的方式,生成类一个ExtensionFactory类。我们来看看ExtensionFactory是如何生成的。

3. 生成ExtensionFactory自适应类

a. getAdaptiveExtensionClass
ExtensionFactory自适应类的创建和其他自适应类的方式相同。

private Class<?> getAdaptiveExtensionClass() {
    //获取ExtensionFactory接口可能的实现类;这一步和JDK一样,Dubbo到规定的几个路径下(比如META-INF/dubbo/internal)去寻找ExtensionFactory实现类
    getExtensionClasses();
    // 如果有哪个实现类标注类@Adaptive,在这里就直接返回
    if (cachedAdaptiveClass != null) {
        return cachedAdaptiveClass;
    }
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

b. 配置文件加载
与JDK SPI不同的是,Dubbo SPI 文件内容是“key=value”的形式:

adaptive=org.apache.dubbo.common.extension.factory.AdaptiveExtensionFactory
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory

getExtensionClasses 方法会读取SPI配置文件,将实现类保存在extensionClasses中

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
    //省略读文件的具体操作
    while ((line = reader.readLine()) != null) {
        ...
        name = line.substring(0, i).trim();//key
        line = line.substring(i + 1).trim();//value
        loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
        ....
    }
}

在loadClass时也有需要注意的地方,不是所有的实现类都会被添加到extenssionClasses中去的。

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) {
    // 如果实现类有Adative注解,则直接将该类缓存起来
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        //就是:cachedAdaptiveClass = clazz;
        cacheAdaptiveClass(clazz);
    }else {
        //  将class保存到extensionClasses中
        saveInExtensionClass(extensionClasses, clazz, name);
    }
}

最终extensionClasses的值如下图:

extensionClasses

再回到getAdaptiveExtensionClass方法中,由于在loadSource时,AdaptiveExtensionFactory上有Adaptive注解,那么cachedAdaptiveClass = AdaptiveExtensionFactory.class。Dubbo直接默认指定了自适应类,直接返回。
于是最终通过getAdaptiveExtensionClass返回的ExtensionFactory返回的是AdaptiveExtensionFactory类。

5. 生成SimpleExt自适应类

和生成ExtensionFactory类似,SimpleExt的自适应类也需要调用getAdaptiveExtension方法,loadSource
a. loadSource
同样也是在"META-INF/dubbo/internal" 路径下加载SimpleExt的实现类

impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2  # Comment 2
impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1#Hello World
impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3 # with head space

在SPI配置文件中定义了三个实现类,我们来看看最终的extensionClasses

SimpleExt

由于在三个实现类,都没有使用Adaptive注解,因此会进入到createAdaptiveExtensionClass方法
b. createAdaptiveExtensionClass
createAdaptiveExtensionClass 通过AdaptiveClassCodeGenerator代码生成器生成代码,然后使用Dubbo定义的compiler将代码文件转换成Class,最终生成自适应类。
代码生成的代码过于复杂,这里只展示生成代码最终的样子,以便理解。

package org.apache.dubbo.common.extension.ext1;
import org.apache.dubbo.common.extension.ExtensionLoader;

public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
    public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1)  {
        throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
}

public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1)  {
    if (arg0 == null) throw new IllegalArgumentException("url == null");
    org.apache.dubbo.common.URL url = arg0;
    String extName = url.getParameter("simple.ext", "impl1");
    if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");

    org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
    return extension.echo(arg0, arg1);
}

public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1)  {
    if (arg0 == null) throw new IllegalArgumentException("url == null");
    org.apache.dubbo.common.URL url = arg0;
    String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
    
    if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
    org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
   
    return extension.yell(arg0, arg1);
}

从上面的代码中,我们可以发现以下几点:

  1. 没有使用@Adaptive的bang方法,直接抛出异常
  2. 自适应interface的方法中,一定要有URL参数:直接( 方法参数中直接有URL参数)、间接( 从参数中通过getUrl方法,获取URL);如果没有URL参数,就会抛出异常
  3. 对于没有指定parameter的方法,如echo方法,在取参数时,会将simple.ext作为key(将SimpleExt拆分开,并用"."隔开);指定类parameter的方法,就按照key的顺序来取。
  4. 自定义类的生成只有在调用方法的时候,才会在方法内生成
  5. 最后会根据key取得的参数,来决定生成哪一个实现类:
    ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName), 最后实现方法的调用。

6. 回到测试

让我们回到最初的测试:

  • 测试1:默认实现
{
    SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
    Map<String, String> map = new HashMap<String, String>();
    URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
    String echo = ext.echo(url, "haha");
    assertEquals("Ext1Impl1-echo", echo);
}

由于SimpleExt在定义时指定默认的自适应类(@SPI("impl1")), 于是即使没有显式指明自适应的类,Dubbo也会为我们生成SimpleExtImpl1。

  • 测试2:使用默认key,指定实现
 {
    SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
    Map<String, String> map = new HashMap<String, String>();
    map.put("simple.ext", "impl2");
    URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
    String echo = ext.echo(url, "haha");
    assertEquals("Ext1Impl2-echo", echo);

测试2中显式指明了要实现的自适应类impl2,于是最终生成的是SimpleExtImpl2.

  • 测试3: 使用自定义key,指定实现;key的优先级是从左到右的
{
    SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
    Map<String, String> map = new HashMap<String, String>();
    map.put("key2", "impl2");
    URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
    String echo = ext.yell(url, "haha");
    assertEquals("Ext1Impl2-yell", echo);
    url = url.addParameter("key1", "impl3"); // note: URL is value's type
    echo = ext.yell(url, "haha");
    assertEquals("Ext1Impl3-yell", echo);
}

yell 方法自定义了key1,key2,于是就不能再使用"simple.ext"作为key了。

  • 测试4: 没有url参数
    SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
    try {
        ext.echo(null, "haha");
        fail();
    } catch (IllegalArgumentException e) {
        assertEquals("url == null", e.getMessage());
    }

在没有url参数的时候,就会抛出异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352

推荐阅读更多精彩内容

  • 一.概览 整体描述 dubbo利用spi扩展机制实现大量的动态扩展,要想充分了解dubbo的扩展机制,首先必须弄明...
    致虑阅读 889评论 0 2
  • 前面我们了解过了Java的SPI扩展机制,对于Java扩展机制的原理以及优缺点也有了大概的了解,这里继续深入一下D...
    加大装益达阅读 5,057评论 2 20
  • dubbo的spi机制 dubbo的扩展点加载机制源自于java的spi扩展机制。那么,何为java的spi扩展机...
    安迪猪阅读 604评论 0 1
  • 从上一篇 Java SPI 机制解析 可以知道 Java SPI 的一些劣势。Dubbo 的扩展点加载从 Java...
    匠丶阅读 4,174评论 0 7
  • Dubbo采用微内核+插件体系,使得设计优雅,扩展性强。那所谓的微内核+插件体系是如何实现的呢!大家是否熟悉spi...
    carl_zhao阅读 936评论 1 3