【Dubbo】SPI的来龙去脉

image.png

JDK的SPI是为了解决什么问题?怎么实现?优缺点?

定义

全程 Service Provider Interface,Service提供者接口,引用别人的一句话 “提供给服务提供厂商与扩展框架功能的开发者使用的接口”
说白了,就是插件式编程

解决问题(引用官网)

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。

实现

image.png

定义服务接口

package dictionary.spi;

public interface Dictionary {
    String getDefinition(String word);
}

服务提供者的实现

package dictionary.spi.impl;

import dictionary.spi.Dictionary;

import java.util.SortedMap;
import java.util.TreeMap;

public class ExtendedDictionary implements Dictionary {
    private SortedMap<String, String> map;

    /**
     * Creates a new instance of ExtendedDictionary
     */
    public ExtendedDictionary() {
        map = new TreeMap<String, String>();
        map.put(
                "xml",
                "a document standard often used in web services, among other " +
                        "things");
        map.put(
                "REST",
                "an architecture style for creating, reading, updating, " +
                        "and deleting data that attempts to use the common " +
                        "vocabulary of the HTTP protocol; Representational State " +
                        "Transfer");
    }

    @Override
    public String getDefinition(String word) {
        return map.get(word);
    }
}

注册服务

在/src/main/resources/META-INF/services/建立dictionary.spi.Dictionary文件
文件内容
dictionary.spi.impl.ExtendedDictionary

调用者


package dictionary;

import dictionary.spi.Dictionary;

import java.util.Iterator;
import java.util.ServiceLoader;

public class DictionaryDemo {

    public static void main(String[] args) {

        ServiceLoader<Dictionary> loader = ServiceLoader.load(Dictionary.class);

        final Iterator<Dictionary> iterator = loader.iterator();
        while (iterator.hasNext()) {
            final Dictionary next = iterator.next();
            System.out.println(next.getDefinition("xml"));
            System.out.println(next.getDefinition("REST"));
        }
    }

}

实现机制

通过上面的例子发现在调用者是基于ServiceLoader这个类实现的调用,我们翻翻这个ServiceLoader的源码
在注释的第一行就写到“A simple service-provider loading facility.”
我们看下的load的方法

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

实际调用

private ServiceLoader(Class<S> svc, ClassLoader cl) {
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }

在reload方法中最关键的是LazyIterator,这里面主要是nextService,里面有下段代码,是通过Class.forName实现

try {
      c = Class.forName(cn, false, loader);
 } catch (ClassNotFoundException x) {
      fail(service,"Provider " + cn + " not found");
 }

缺点

  • JDK的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • JDK spi不支持默认值
  • 要用for循环判断对象

Dubbo为什么又实现了一套SPI?是解决了问题?

针对JDK的问题,dubbo又实现SPI,主要解决下面问题

1.支持缓存对象:spi的key与value 缓存在 cachedInstances对象里面,它是一个ConcurrentMap
2.dubbo设计默认值:@SPI("dubbo") 代表默认的spi对象,例如Protocol的@SPI("dubbo")就是 DubboProtocol,
通过 ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension()那默认对象
3.根据KEY直接获取
4.设计增加了AOP功能,在cachedWrapperClasses,在原始spi类,包装了XxxxFilterWrapper XxxxListenerWrapper
5.dubbo设计增加了IOC,通过构造函数注入,代码为:wrapperClass.getConstructor(type).newInstance(instance),

Dubbo根据JDK SPI机制的思想实现了自己的扩展点机制,ExtensionLoader是Dubbo扩展点机制的核心,相当于JDK SPI中的ServiceLoader

如何使用

 public void test_useAdaptiveClass() throws Exception {
        ExtensionLoader<HasAdaptiveExt> loader = ExtensionLoader.getExtensionLoader(HasAdaptiveExt.class);
        HasAdaptiveExt ext = loader.getAdaptiveExtension();
        assertTrue(ext instanceof HasAdaptiveExt_ManualAdaptive);
    }
/**
 * @author ding.lid
 */
@SPI
public interface HasAdaptiveExt {
    @Adaptive
    String echo(URL url, String s);
}

public class HasAdaptiveExtImpl1 implements HasAdaptiveExt {
    public String echo(URL url, String s) {
        return this.getClass().getSimpleName();
    }
}

@Adaptive
public class HasAdaptiveExt_ManualAdaptive implements HasAdaptiveExt {
    public String echo(URL url, String s) {
        HasAdaptiveExt addExt1 = ExtensionLoader.getExtensionLoader(HasAdaptiveExt.class).getExtension(url.getParameter("key"));
        return addExt1.echo(url, s);
    }
}

在src/test/resources/META-INF/dubbo/internal目录下,有一个文件com.alibaba.dubbo.common.extensionloader.adaptive.HasAdaptiveExt
内容如下

adaptive=com.alibaba.dubbo.common.extensionloader.adaptive.impl.HasAdaptiveExt_ManualAdaptive
impl1=com.alibaba.dubbo.common.extensionloader.adaptive.impl.HasAdaptiveExtImpl1
配置格式:配置名=扩展实现类全限定名

通过上面的例子,我们可以看出:

先通过ExtensionLoader.getExtensionLoader加载一个接口
通过getAdaptiveExtension找到一个自适应的类
找到具体的实现类

  • 注解的含义
  1. @SPI,扩展点接口的标识
扩展点声明配置文件,格式修改。 以Protocol示例,配置文件META-INF/dubbo/com.xxx.Protocol内容: 由 
com.foo.XxxProtocol
   com.foo.YyyProtocol
 改成使用KV格式 
xxx=com.foo.XxxProtocol
   yyy=com.foo.YyyProtocol

注意:当扩展点的static字段或方法签名上引用了三方库, 如果三方库不存在,会导致类初始化失败, Extension标识Dubbo就拿不到了,异常信息就和配置对应不起来。 比如: Extension("mina")加载失败, 当用户配置使用mina时,就会报找不到扩展点, 而不是报加载扩展点失败,以及失败原因。(dubbo的注释)

  1. @Adaptive (方法上或类上)
    Dubbo使用的扩展点获取。
  • 自动注入关联扩展点。
  • 自动Wrap上扩展点的Wrap类。
  • 缺省获得的的扩展点是一个Adaptive Instance。

我们根据ExtensionLoader的源码来分析下Dubbo是如何实现的SPI

先看下ExtensionLoader#getExtensionLoader方法(静态)

/**
     * <ul>
     * <li>首先判断扩展点类型是否为空</li>
     * <li>判断是否是接口</li>
     * <li>判断是否标注了SPI注解</li>
     * <li>该扩展点是否已经创建过加载器实例</li>
     * <li>如果没有被创建,创建过加载器实例,并且放到缓存中</li>
     * </ul>
     *
     * @param type 接口类型
     * @param <T>
     * @return 具体对象
     */
    @SuppressWarnings("unchecked")
    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 interface!");
        }
        if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type +
                    ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        }

        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;
    }
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

这里使用了ConcurrentMap,缓存了ExtensionLoader,key就是接口类型

在 EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
这句调用了

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

ExtensionFactory 就实现了SPI:


屏幕快照 2017-12-07 下午3.54.36.png

自适应 getAdaptiveExtension()

获取一个扩展类,如果@Adaptive注解在类上就是一个装饰类;如果注解在方法上就是一个动态代理类,例如Protocol$Adaptive对象。

 @SuppressWarnings("unchecked")
    public T getAdaptiveExtension() {
        //先从实例缓存中查找实例对象
        Object instance = cachedAdaptiveInstance.get();
        // 用来缓存自适应实现类的实例
        //缓存中不存在 这里采用了两次检查
        if (instance == null) {
            if (createAdaptiveInstanceError == null) {
                synchronized (cachedAdaptiveInstance) {
                    instance = cachedAdaptiveInstance.get();
                    if (instance == null) {
                        try {
                            // if为空,创建新的实例
                            instance = createAdaptiveExtension();
                            cachedAdaptiveInstance.set(instance);
                        } catch (Throwable t) {
                            createAdaptiveInstanceError = t;
                            throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
                        }
                    }
                }
            } else {
                throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }
@SuppressWarnings("unchecked")
    private T createAdaptiveExtension() {
        try {
            return injectExtension((T) getAdaptiveExtensionClass().newInstance());
        } catch (Exception e) {
            throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
        }
    }

getAdaptiveExtensionClass()


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

    private Class<?> createAdaptiveExtensionClass() {
        // /组装自适应扩展点类的代码
        String code = createAdaptiveExtensionClassCode();
        //获取到应用的类加载器
        ClassLoader classLoader = findClassLoader();
        //获取编译器 默认使用javassist
        com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
        // 将代码转换成Class
        return compiler.compile(code, classLoader);
    }

编译器的结构图:


屏幕快照 2017-12-07 下午4.31.35.png
/**
     * 组装自适应扩展点类的代码
     *
     * @return
     */
    private String createAdaptiveExtensionClassCode() {
       // 省略代码
    }

injectExtension

/**
     * 注入
     * @param instance
     * @return
     */
    private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                // 遍历实例的所有方法
                for (Method method : instance.getClass().getMethods()) {
                    // 必须是public 且 set 且 有参数
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                        Class<?> pt = method.getParameterTypes()[0];
                        try {
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            // 根据ExtensionFactor 创建对象
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) {
                                method.invoke(instance, object);
                            }
                        } catch (Exception e) {
                            logger.error("fail to inject via method " + method.getName()
                                    + " of interface " + type.getName() + ": " + e.getMessage(), e);
                        }
                    }
                }
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
        return instance;
    }
Object object = objectFactory.getExtension(pt, property); 

这里面我们用spring的为例子

  public <T> T getExtension(Class<T> type, String name) {
        for (ApplicationContext context : contexts) {
            if (context.containsBean(name)) {
                Object bean = context.getBean(name);
                if (type.isInstance(bean)) {
                    return (T) bean;
                }
            }
        }
        return null;
    }

总结

实现步骤

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

推荐阅读更多精彩内容