dubbo系列-扩展点机制-Dubbo SPI

相信对dubbo有过了解的小伙伴应该知道,dubbo之所以被广泛的使用,其中最重要的一个原因是因为其优秀的可扩展性。而如此良好的扩展性有两个密不可分的原因,一个是设计模式,另一个就是dubbo自身独特的扩展点机制-dubbo SPI,本文将主要从以下几个方面来详细解读dubbo SPI的实现机制。

  • java spi
  • dubbo 中spi优化与特性
  • 源码解读扩展点注解
  • 总结

一、java spi

在讲解Dubbo SPI之前,先了解一下Java SPI是怎么使用的。SPI的全称是Service Provider Interface,起初是提供给厂商做插件开发的。通俗点解释其实就是策略模式,定义一个接口,有多个实现,只不过接口对应的实现不在代码中直接声明,而是通过配置文件来配置这个对应实现的关系。具体步骤如下:

(1) 定义一个接口及对应的方法。 (2) 编写该接口的一个实现类。
(3) 在META-INF/services/目录下,创建一个以接口全路径命名的文件,如com.test.spi.PrintService
(4) 文件内容为具体实现类的全路径名,如果有多个,则用分行符分隔。
(5) 在代码中通过java.util.ServiceLoader来加载具体的实现类。

Java SPI示例代码

public interface Printservice ( <——① SPI 接口定义
       void printlnfo();
}

public class PrintServicelmpl implements Printservice { _②SPI接口实现类
       Override
       public void printlnfo() {
           System.out.println("hello world");
       }
 }

public static void main(String[] args) (  //调用SPI具体的实现
   ServiceLoader<PrintService> serviceServiceLoader =
   ServiceLoader.load(PrintService.class);
   for (Printservice printservice : serviceServiceLoader) ( <-------------
       //此处会输出:hello world 获取所有的SPI实现,循环调用
       printService.printInfo(); 
   } 
}

dubbo官方文档中对于java spi的缺点给出了一下两点

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

二、dubbo spi

针对以上两点,我们来看dubbo做了哪些优化:

2.1 按需获取扩展点实现

  • 在上文java spi的演示代码中我们看到,java.util.ServiceLoader会一次把Printservice接口下的所有实现类全部初始化。用户直接调用即可。Dubbo SPI只是加载配置文件中的类, 并分成不同的种类缓存在内存中,而不会立即全部初始化,在性能上有更好的表现。具体的实现原理会在后面讲解,此处演示一个使用示例。

PrintService接口的Dubbo SPI改造

PrintService接口的Dubbo SPI改造
① 在目录META-INF/dubbo/internal下建立配置文com.test.spi.Printservice,文件内容如下
impl=com.test.spi.PrintServiceImpl 

② 为接口类添加SPI注解,设置默认实现为impl
@SPI("impl")   
public interface Printservice {
    void printlnfo();
)

③实现类不变
public class PrintServicelmpl implements Printservice ( 
Override
public void printlnfo() (
      System.out println("hello world");
} 
}

④调用Dubbo SPI
public static void main(String[] args) ( 
    //通过 ExtensionLoader 获取接口PrintService.class 的默认实现
    PrintService printservice = ExtensionLoader
    .getExtensionLoader(PrintService.class).getDefaultExtension();
    //此处会输出 PrintServicelmpl 打印的 hello world
    printService.printInfo();
}

我们发现,在dubbo中,如果一个扩展点有多个实现,我们可以不毕直接加载所有实现,而是根据自己的需要获取扩展点实现,具体实现原理下文我们通过源码分析。

2.2、异常处理

Java SPI加载失败,可能会因为各种原因导致异常信息被“吞掉”,导致开发人员问题追踪比较困难。Dubbo SPI在扩展加载失败的时候会先抛出真实异常并打印日志。扩展点在被动加载的时候,即使有部分扩展加载失败也不会影响其他扩展点和整个框架的使用

2.3、IOC和AOP机制

  • 在dubbo中,很多功能都是通过扩展点来实现的,既然如此,一旦扩展点很多的话,扩展点之间的依赖关系怎么处理也是一个问题,而在dubbo中则是通过自动装配,即如果实例化一个扩展点实现,会继续看有没有依赖此扩展点的扩展点。判断方法也很简单,就是通过set方法,即一个扩展点可以通过setter方法直接注入其他扩展点。这个和spring的IOC原理类似,所以我们称它为dubbo spi中的IOC,也称为扩展点的自动扩展特性,具体实现逻辑方法为 injectExtension(T instance),后面会详细解释。这么说有点抽象,我们来看官网中给出的具体示例:
    有两个为扩展点 CarMaker(造车者)、WheelMaker (造轮者)
public interface CarMaker {
    Car makeCar();
}
 
public interface WheelMaker {
    Wheel makeWheel();
}

CarMaker 的一个实现类

public class RaceCarMaker implements CarMaker {
    WheelMaker wheelMaker;
 
    public void setWheelMaker(WheelMaker wheelMaker) {
        this.wheelMaker = wheelMaker;
    }
 
    public Car makeCar() {
        // ...
        Wheel wheel = wheelMaker.makeWheel();
        // ...
        return new RaceCar(wheel, ...);
    }
}

ExtensionLoader 加载 CarMaker 的扩展点实现 RaceCarMaker 时,setWheelMaker 方法的 WheelMaker 也是扩展点则会注入 WheelMaker 的实现。
这里带来另一个问题,ExtensionLoader 要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker 的实现中要注入哪个。
这个问题在下面一点 [扩展点自适应]特性的时候讲解

  • 熟悉设计模式的应该知道,装饰者模式是很常用的一种设计模式。它通常用来在不改变原有对象的行为方法的同时,用来对原有对象进行方法增强。而在dubbo中,实例化一个扩展点实现的同时,也会判断此对象有没有作为一个包装类对象中构造方法的对象,如果是,也会实例化该 wrapper包装类。举例说明:
private T createExtension(String name){
             //这里省略了部分代码
             .......
   /**
             * 向扩展类注入其依赖的扩展点属性,这里是体现了扩展点自动装配的特性
             */
            injectExtension(instance);
            /**
             * 这里的cachedWrapperClasses对象 在执行getExtensionClasses方法时已经赋值
             * 扩展点自动包装特性,ExtensionLoader在加载扩展时,如果发现这个扩展类包含其他扩展点作为构造函数的参数,
             * 则这个扩展类会被认为是wrapper类,比如 ProtocolFilterWrapper,就是在构造函数中注入了 Protocol类型的扩展点
             * 那么这个wrapper类也会被实例化并且注入扩展点属性
             */
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (CollectionUtils.isNotEmpty(wrapperClasses)) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    //wrapper对象的实例化(injectExtension():向扩展类注入其依赖的属性,如扩展类A又依赖了扩展类B,那么就向A中注入扩展类B)
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }

           .......

}

在上面这个创建扩展点的方法中,扩展点自动装配以后,会继续对包装类对象进行注入。这个和aop原理一样,称为dubbo spi中的aop,也叫扩展点的自动包装

三、源码解读扩展点注解

上文大致说明了dubbo spi对于java spi的优化点,下面我们针对优化点进行一波源码分析。

3.1、@SPI 注解源码解读

首先看下按需获取获取扩展点实现。这里涉及到一个注解 @SPI,在上文Dubbo SPI改造示例代码中也有体现,@SPI注解可以使用在类、接口和枚举类上,Dubbo框架中都是使用在接口上。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * default extension name
     */
    String value() default "";

}

我们可以看到SPI注解有一个value属性,通过这个属性,我们可以传入不同的参数来设置这个接口的默认实现类。例如,我们可以看到Transporter接口使用Netty作为默认实现

@SPI(”netty”)
public interface Transporter!
}

下面具体看一下是怎么根据名称获取到具体的实现类的,代码入口:
org.apache.dubbo.common.extension.ExtensionLoader#getExtension

    public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        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;
    }
/**
 * 1、加载定义文件中的各个子类,然后将目标name对应的子类返回后进行实例化。
 * 2、通过目标子类的set方法为其注入其所依赖的bean,这里既可以通过SPI,也可以通过Spring的BeanFactory获取所依赖的bean,injectExtension(instance)。
 * 3、获取定义文件中定义的wrapper对象,然后使用该wrapper对象封装目标对象,并且还会调用其set方法为wrapper对象注入其所依赖的属性
 * @param name
 * @return
 */
@SuppressWarnings("unchecked")
private T createExtension(String name) {

    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        /**
         * 向扩展类注入其依赖的扩展点属性,这里是体现了扩展点自动装配的特性
         */
        injectExtension(instance);
        /**
         * 这里的cachedWrapperClasses对象 在执行getExtensionClasses方法时已经赋值
         * 扩展点自动包装特性,ExtensionLoader在加载扩展时,如果发现这个扩展类包含其他扩展点作为构造函数的参数,
         * 则这个扩展类会被认为是wrapper类,比如 ProtocolFilterWrapper,就是在构造函数中注入了 Protocol类型的扩展点
         * 那么这个wrapper类也会被实例化并且注入扩展点属性
         */
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (CollectionUtils.isNotEmpty(wrapperClasses)) {
            for (Class<?> wrapperClass : wrapperClasses) {
                //wrapper对象的实例化(injectExtension():向扩展类注入其依赖的属性,如扩展类A又依赖了扩展类B,那么就向A中注入扩展类B)
                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);
    }
}

createExtension是个关键的方法:此方法基本分为三个步骤:

1、加载定义文件中的各个子类,然后将目标name对应的子类返回后进行实例化。
2、通过目标子类的set方法为其注入其所依赖的bean,这里既可以通过SPI,也可以通过Spring的BeanFactory获取所依赖的bean,injectExtension(instance)。
3、获取定义文件中定义的wrapper对象,然后使用该wrapper对象封装目标对象,并且还会调用其set方法为wrapper对象注入其所依赖的属性

1、getExtensionClasses 方法比较长,在此不一一列举,主逻辑就是获取到扩展点的所有实现类,中间会加入各种缓存提高性能,需要注意的是这里获取的只是扩展点的实现类,并没有实例化,这也印证了我们上面所说的按照需要获取扩展实现类,并且只是加载配置文件中的类, 并分成不同的种类缓存在内存中,而不会立即全部初始化,在性能上有更好的表现。

2、injectExtension 是扩展点自动扩展的特性具体实现,基本原理:

方法总体实现了类似Spring的IoC机制,其实现原理比较简单:首先通
过反射获取类的所有方法,然后遍历以字符串set开头的方法,得到set方法的参数类型,再通过ExtensionFactory寻找参数类型相同的扩展类实例,如果找到,就设值进去

/**
 * 注入扩展类
 * 像扩展类中注入其依赖的属性,如扩展类A又依赖了扩展类B,那么就向A中注入扩展类B
 *
 *injectExtension方法总体实现了类似Spring的IoC机制,其实现原理比较简单:首先通
 * 过反射获取类的所有方法,然后遍历以字符串set开头的方法,得到set方法的参数类型,再通
 * 过ExtensionFactory寻找参数类型相同的扩展类实例,如果找到,就设值进去
 * @param instance
 * @return
 */
private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            for (Method method : instance.getClass().getMethods()) {
                if (isSetter(method)) {
                    /**
                     * Check {@link DisableInject} to see if we need auto injection for this property
                     */
                    if (method.getAnnotation(DisableInject.class) != null) {
                        continue;
                    }
                    Class<?> pt = method.getParameterTypes()[0];
                    if (ReflectUtils.isPrimitives(pt)) {
                        continue;
                    }
                    try {
                        String property = getSetterProperty(method);
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            //执行set方法
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("Failed to inject via method " + method.getName()
                                + " of interface " + type.getName() + ": " + e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

3.2、扩展点自适应注解:©Adaptive

上文中我们遗留了一个问题,ExtensionLoader 要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。这就需要提到扩展点的自适应注解,©Adaptive。

@Adaptive注解可以标记在类、接口、枚举类和方法上,但是在整个Dubbo框架中,只有几个地方使用在类级别上,如AdaptiveExtensionFactory和AdaptiveCompiler,其余都标注在方法上。如果标注在接口的方法上,即方法级别注解,则可以通过参数动态获得实现类,方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
    //数组,可以设置多个key,会按顺序依次匹配
    String[] value() default {};
}

该注解也可以传入value参数,是一个数组。我们在代码清单4.9中可以看到,Adaptive可以传入多个key值,在初始化Adaptive注解的接口时,会先对传入的URL进行key值匹配,第一个key没匹配上则匹配第二个,以此类推。直到所有的key匹配完毕,如果还没有匹配到, 则会使用“驼峰规则”匹配,如果也没匹配到,则会抛出IllegalStateException异常。 什么是"驼峰规则”呢?如果包装类(Wrapper 没有用Adaptive指定key值,则Dubbo会自动把接口名称根据驼峰大小写分开,并用符号连接起来,以此来作为默认实现类的名称,如下面示例中的 SimpleExt 会被转化为simple.ext。

对于@Adaptive 注解的解析可以从这个单元测试着手:
org.apache.dubbo.common.extension.ExtensionLoader_Adaptive_Test#test_getAdaptiveExtension_defaultAdaptiveKey

@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);
}

public class SimpleExtImpl1 implements SimpleExt {
    public String echo(URL url, String s) {
        return "Ext1Impl1-echo";
    }

    public String yell(URL url, String s) {
        return "Ext1Impl1-yell";
    }

    public String bang(URL url, int i) {
        return "bang1";
    }
}

public class SimpleExtImpl2 implements SimpleExt {
    public String echo(URL url, String s) {
        return "Ext1Impl2-echo";
    }

    public String yell(URL url, String s) {
        return "Ext1Impl2-yell";
    }

    public String bang(URL url, int i) {
        return "bang2";
    }

}

public class SimpleExtImpl3 implements SimpleExt {
    public String echo(URL url, String s) {
        return "Ext1Impl3-echo";
    }

    public String yell(URL url, String s) {
        return "Ext1Impl3-yell";
    }

    public String bang(URL url, int i) {
        return "bang3";
    }

}
@Test
public void test_getAdaptiveExtension_defaultAdaptiveKey() throws Exception {
    {
        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 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);
    }
}
    public T getAdaptiveExtension() {
        Object instance = cachedAdaptiveInstance.get();
        if (instance == null) {
            if (createAdaptiveInstanceError == 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);
                        }
                    }
                }
            } else {
                throw new IllegalStateException("Failed to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
            }
        }

        return (T) instance;
    }
    private Class<?> getAdaptiveExtensionClass() {
        //加载所有扩展点实现类
        getExtensionClasses();
        if (cachedAdaptiveClass != null) {
            return cachedAdaptiveClass;
        }
        //此处是关键的一步,生成自适应扩展类
        return cachedAdaptiveClass = createAdaptiveExtensionClass();
    }

createAdaptiveExtensionClass是关键的一步:

为扩展点接口自动生成实现类字符串,实现类主要包含以下逻辑:为接口中每个有Adaptive注解的方法生成默认实现(没有注解的方法则生成空实现),每个默认实现都会从URL中提取Adaptive参数值,并以此为依据动态加载扩展点。然后,框架会使用不同的编译器,把实现类字符串编译为自适应类并返回

    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);
    }
    }

通过debug我们可以看到生成的类字符串到底长什么样,这里我们贴出来看一下:

     * package org.apache.dubbo.common.extension.support;
     * 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 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);
     *     }
     *     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);
     *     }
     * }

由上面动态生成的$Adaptive类可以得知,每个默认实现都会从URL中提取Adaptive参数值,并以此为依据动态加载扩展点,如示例所示,就是根据url中获取到extName参数,然后调用 getExtension(extName)
如果一个接口上既有@SPI(”impl2”)注解,方法上又有@Adaptive(”impl1”)注解,那么会以哪个key作为默认实现呢?由上面动态生成的Adaptive类可以得知,最终动态生成的实现方法会是url.getParameter(simple.ext, "impl”),即优先通过©Adaptive注解传入的key去查找扩展实现类;如果没找到,则通过@SPI注解中的key去查找;如果@SPI注解中没有默认值,则把类名转化为key,再去查找。

除了上面的这个示例,还有一个示例可以参考,我们看dubbo中的传输协议,接口默认为dubbo的传输协议,在服务暴露和引用的方法中,加了@Adaptive注解。

@SPI("dubbo")
public interface Protocol {
    int getDefaultPort();

    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

以上获取 protocol的代码,会生成一个中间代理类如下:

package org.apache.dubbo.rpc;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol {
    public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException {
        if (arg0 == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null) throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null");
        org.apache.dubbo.common.URL url = arg0.getUrl();
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
    public void destroy()  {
        throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
    public int getDefaultPort()  {
        throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!");
    }
    public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg1;
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])");
        org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }
}

和上述例子相同,在动态获取协议的过程中,也是通过url中的参数来动态获取,如果url中protocol参数为空,则默认使用dubbo协议,如果不为空,比如服务暴露的过程中,先本地暴露,那么url中protocol = injvm,那么获取到的协议就是InjvmProtocol。

3.3、扩展点自动激活注解:©Active

上面我们提到自适应的扩展实现机制动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现类同时被激活,如Filter可以同时有多个过滤器;或者根据不同的条件,同时激活多个实现类, 如何实现?这就涉及最后一个特性一一自动激活

使用@Activate注解,可以标记对应的扩展点默认被激活启用。该注解还可以通过传入不同的参数,设置扩展点在不同的条件下被自动激活。主要的使用场景是某个扩展点的多个实现类需要同时启用(比如Filter扩展点)

@Active 可以传入的参数很多

active参数
对于@Active注解的解析可以从这个org.apache.dubbo.common.extension.ExtensionLoaderTest#testLoadActivateExtension 测试用来来进行解析。

public List<T> getActivateExtension(URL url, String[] values, String group) {
    List<T> exts = new ArrayList<>();
    List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
    if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
        //获取扩展点所有实现类
        getExtensionClasses();
        //遍历整个@Acitve注解集合
        for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
            String name = entry.getKey();
            Object activate = entry.getValue();

            String[] activateGroup, activateValue;

            if (activate instanceof Activate) {
                activateGroup = ((Activate) activate).group();
                activateValue = ((Activate) activate).value();
            } else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
                activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
                activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
            } else {
                continue;
            }
            //group条件匹配
            if (isMatchGroup(group, activateGroup)) {
                T ext = getExtension(name);
                if (!names.contains(name)
                        && !names.contains(REMOVE_VALUE_PREFIX + name)
                        && isActive(activateValue, url)) {
                    exts.add(ext);
                }
            }
        }
        //排序
        exts.sort(ActivateComparator.COMPARATOR);
    }
    List<T> usrs = new ArrayList<>();
    for (int i = 0; i < names.size(); i++) {
        String name = names.get(i);
        if (!name.startsWith(REMOVE_VALUE_PREFIX)
                && !names.contains(REMOVE_VALUE_PREFIX + name)) {
            if (DEFAULT_KEY.equals(name)) {
                if (!usrs.isEmpty()) {
                    exts.addAll(0, usrs);
                    usrs.clear();
                }
            } else {
                T ext = getExtension(name);
                usrs.add(ext);
            }
        }
    }
    if (!usrs.isEmpty()) {
        exts.addAll(usrs);
    }
    return exts;
}

该方法主线流程分为4步:

(1) 依旧是获取扩展点的所有实现
(2) 遍历整个©Activate注解集合,根据传入URL匹配条件(匹配group> name等),得
到所有符合激活条件的扩展类实现。然后根据@Active中配置的before、after、order等参数进行排序
(3) 遍历所有用户自定义扩展类名称,根据用户URL配置的顺序,调整扩展点激活顺序
(遵循用户在 URL 中配置的顺序,例如 URL 为 test ://localhost/test?ext=orderlJdefault,则扩展点ext的激活顺序会遵循先order再default,其中default代表所有有@Activate注解的扩展点)。
(4) 返回所有自动激活类集合。

四、总结

好了,撸了这么久源码,总结一波:

dubbo Spi为了优化java spi的缺点,引入了可以按照需要获取扩展点的实现类。其中@SPI注解通常用在接口上,指定扩展点的实现类。并且在加载扩展点的过程中,引入了自动扩展和自动封装扩展点的特性,在自动扩展的过程过程中,因为需要确定自动扩展的是具体哪一个实现类,
又引入了@Adaptive注解,该注解大部分用在方法级别,通过生成动态的$Adaptive类来解析参数,并通过参数动态获取具体的实现类。但是其自适应扩展点实现只能一个,
又引入了@Active注解,此注解可以通过传入不同的参数,设置扩展点在不同的条件下多个实现类被自动激活,主要的使用场景是某个扩展点的多个实现类需要同时启用(比如Filter扩展点)

备注:文中示例代码版本:2.7.3
参考文献:
1、dubbo官网
2、书籍:深入理解Apache Dubbo与实战

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

推荐阅读更多精彩内容