Fork me on GitHub

Dubbo扩展机制实现(二)之浅析ExtensionLoader

前言

之前的文章有介绍jdk SPI一些基本的使用和源码分析,既然dubbo也想使用SPI机制,为什么不直接使用jdk的SPI呢?

上篇文章开头也提到了:

Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。

看看官方文档上Dubbo加强了哪些地方:

本文简单分析下Dubbo实现扩展点机制的ExtensionLoader类,分析其对比java的spi是怎么改进的。

Dubbo扩展点约定

在扩展类的 jar 包内 ,放置扩展点配置文件 META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。

注意:这里的配置文件是放在你自己的 jar 包内,不是 dubbo 本身的 jar 包内,Dubbo 会全 ClassPath 扫描所有 jar 包内同名的这个文件,然后进行合并 ↩

一个自定义扩展点小例子

第一步:新建一个jar包,我这里是在原先的dubbo源码包里的dubbo-rpc模块新增了一个实现 [dubbo-rpc-myrpc]:

<parent>
    <artifactId>dubbo-rpc</artifactId>
    <groupId>com.alibaba</groupId>
    <version>2.6.1</version>
</parent>

<artifactId>dubbo-rpc-myrpc</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>The my rpc module of dubbo project</description>
<properties>
    <skip_maven_deploy>false</skip_maven_deploy>
</properties>
<dependencies>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo-rpc-api</artifactId>
        <version>${project.parent.version}</version>
    </dependency>
</dependencies>

第二步:以实现Protocol扩展为例,新建自定义实现:

/**
 * MyRpcProtocol
 */
public class MyRpcProtocol extends AbstractProtocol implements Protocol {

    public static final int DEFAULT_PORT = 0;

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

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        System.out.println("my rpc export...");
        return null;
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        System.out.println("my rpc refer...");
        return null;
    }
}

第三步:在jar包里定义扩展点:META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol

myrpc=com.alibaba.dubbo.rpc.protocol.myrpc.MyRpcProtocol

大工完成,是不是so easy…

第四步:测试。接下来测试一下,写一个提供者使用我们的自定义协议。
pom.xml里引用我们的自定义包

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>dubbo-rpc-myrpc</artifactId>
    <version>2.6.1</version>
</dependency>

提供者xml里注册我们的协议:

<dubbo:protocol name="myrpc" port="66666"/>

然后启动服务:

public class Provider {

    public static void main(String[] args) throws Exception {
        //Prevent to get IPV6 address,this way only work in debug mode
        //But you can pass use -Djava.net.preferIPv4Stack=true,then it work well whether in debug mode or not
        System.setProperty("java.net.preferIPv4Stack", "true");
        ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"META-INF/spring/dubbo-demo-provider.xml"});
        context.start();

        System.in.read(); // press any key to exit
    }

}

可以看到控制台打印了:

...

[11/06/18 11:12:16:016 CST] main  INFO config.AbstractConfig:  [DUBBO] Export dubbo service com.alibaba.dubbo.demo.DemoService to local registry, dubbo version: 2.0.0, current host: 172.16.192.43
[11/06/18 11:12:16:016 CST] main  INFO config.AbstractConfig:  [DUBBO] Export dubbo service com.alibaba.dubbo.demo.DemoService to url myrpc://172.16.192.43:66666/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider2&bind.ip=172.16.192.43&bind.port=66666&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=25280&qos.port=22222&side=provider&timestamp=1528686736167, dubbo version: 2.0.0, current host: 172.16.192.43
[11/06/18 11:12:16:016 CST] main  INFO config.AbstractConfig:  [DUBBO] Register dubbo service com.alibaba.dubbo.demo.DemoService url myrpc://172.16.192.43:66666/com.alibaba.dubbo.demo.DemoService?anyhost=true&application=demo-provider2&bind.ip=172.16.192.43&bind.port=66666&dubbo=2.0.0&generic=false&interface=com.alibaba.dubbo.demo.DemoService&methods=sayHello&pid=25280&qos.port=22222&side=provider&timestamp=1528686736167 to registry registry://127.0.0.1:2181/com.alibaba.dubbo.registry.RegistryService?application=demo-provider2&dubbo=2.0.0&pid=25280&qos.port=22222&registry=zookeeper&timestamp=1528686735588, dubbo version: 2.0.0, current host: 172.16.192.43
my rpc export...
Exception in thread "main" java.lang.IllegalArgumentException: exporter == null
    at com.alibaba.dubbo.rpc.listener.ListenerExporterWrapper.<init>(ListenerExporterWrapper.java:40)
    at com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper.export(ProtocolListenerWrapper.java:59)

...

至此说明了我们的自定义扩展点可以使用。这样以后如果需要实现自定义的一些其他扩展点,使用起来也是非常easy.这里可以体现出Dubbo的设计理念:

  • API 与 SPI 分离
  • 微核插件式,平等对待第三方

ExtensionLoader中的缓存

Dubbo官方文档也说了,扩展点的实例化并非一次性全部加载的。所以它可能是懒加载的,用到哪个实例化哪个扩展点,其次官方文档也说了Dubbo的扩展点性能提升不少,说到性能提升下意识就是想到万能的缓存。来看看 Dubbo的扩展点加载器 ExtensionLoader是怎么实现的提高性能的。

ExtensionLoader各式各样的缓存:

public class ExtensionLoader<T> {
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
    private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>();
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
    private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>();
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
    private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>();
    private volatile Class<?> cachedAdaptiveClass = null;
    private String cachedDefaultName;
    private volatile Throwable createAdaptiveInstanceError;
    private Set<Class<?>> cachedWrapperClasses;
    private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<String, IllegalStateException>();

    ...
}

ExtensionLoader并没有提供public的构造器,获取一个ExtensionLoader实例是通过私有静态方法 getExtensionLoader(Class type) 法获取。

private ExtensionLoader(Class<?> type) {
    this.type = type;
    /** 
     * 这里会存在递归调用,ExtensionFactory的objectFactory为null,其他则为AdaptiveExtensionFactory
     * AdaptiveExtensionFactory的factories中有SpiExtensionFactory,SpringExtensionFactory
     * getAdaptiveExtension()来获取一个拓展装饰类对象
     * objectFactory是一个 ExtensionFactory 对象,扩展点工厂类,暂且不分析
     */
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

@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)) { // 必须使用@spi注解,否则抛异常
        throw new IllegalArgumentException("Extension type(" + type +
                ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }

    // 使用了缓存,从缓存EXTENSION_LOADERS中获取,如果不存在则创建后加入缓存,每个扩展点有且仅有一个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;
}

Dubbo处理缓存的一些值得学习的小细节:对线程安全方面的细节做得很好。

  1. 比如缓存都使用 ConcurrentMap 而不使用 HashMap.
  2. volatile关键字的使用。

    private volatile Class<?> cachedAdaptiveClass = null;
    
    public class Holder<T> {
     private volatile T value;
     public void set(T value) {
         this.value = value;
    
     public T get() {
         return value;
     }
    }
    

Dubbo如何改进获取spi的问题

问题一:JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源

答:Dubbo的ExtensionLoader提供了三种获取扩展点实现类的方式:

  • public T getExtension(String name)
    根据名称获取当前扩展的指定实现
  • public T getAdaptiveExtension()
    获取当前扩展点的自适应实现
  • public List getActivateExtension(URL url, String[] values, String group)
    获取可激活的扩展点集合

这三个地方准备在下一篇扩展点自适应自动激活的分析时一并讲解一下。 这里可以看到Dubbo可以直接根据key就能获取到spi对象,而java的spi只能通过遍历然后根据if判断才能获取制定的spi对象。时间复杂度O(1) 比 O(n)快不少。而且用到了就加到缓存里,不用就不需要实例化,节约资源。

问题二:如果扩展点加载失败,连扩展点的名称都拿不到了。会把真正失败的原因吃掉

答: Dubbo并不会这样,当拿不到扩展点的名字时,Dubbo会直接抛出异常:

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    ...
}

其次,Dubbo非常好的一点,增加了默认值的设置。 比如:

@SPI("dubbo")
public interface Protocol {}

这样就默认提供了dubbo=xxx.xxx.XxxProtocol的s实现。如果使用默认的扩展点,可以这么做:

Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();

这里的protocol对象即是com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

问题三:增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点

答:这个之后分析。

总结

  • 实现一个Dubbo自定义扩展点只需要三步。
  • Dubbo中的缓存设计在线程安全方面非常值得学习。
  • Dubbo是如何加强java的spi的,java的spi上哪些的不足被Dubbo巧妙实现了。

后记

本文只是简单介绍了一下扩展点加载器ExtensionLoader。之后还有更多的源码分析它。我会从Dubbo官方文档上写的四个特性分析它并借鉴其中的一些理念。

下一篇就说说扩展点的四个特性: 扩展点自动包装,扩展点自动装配,扩展点自适应,扩展点自动激活。

-------------本文结束,感谢您的阅读-------------
贵在坚持,如果您觉得本文还不错,不妨打赏一下~
0%