vlambda博客
学习文章列表

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 == nullthrow new IllegalArgumentException("url == null");
     *         org.apache.dubbo.common.URL url = arg0;
     *         String extName = url.getParameter("key1", url.getParameter("key2""impl1"));
     *         if(extName == nullthrow 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 == nullthrow new IllegalArgumentException("url == null");
     *         org.apache.dubbo.common.URL url = arg0;
     *         String extName = url.getParameter("simple.ext""impl1");
     *         if(extName == nullthrow 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 == nullthrow new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == nullthrow 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 == nullthrow 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 == nullthrow new IllegalArgumentException("url == null");
        org.apache.dubbo.common.URL url = arg1;
        String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
        if(extName == nullthrow 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 可以传入的参数很多

Dubbo高可用的扩展点机制 - Dubbo SPI

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与实战

来源:https://www.jianshu.com/p/317ea9559ee2















END











Dubbo高可用的扩展点机制 - Dubbo SPI

点分享

Dubbo高可用的扩展点机制 - Dubbo SPI

点收藏

点点赞

点在看