dubbo系列之dubbo SPI

Dubbo 版本2.7.0

为什么先讲 SPI ? 因为 Dubbo 的拓展实现就是采用这一种机制。

SPI 是一种服务发现机制,全称为 “Service Provider Interface”。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。Dubbo 则利用此特性为程序提供拓展功能,不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。

所以,基于 SPI 机制,我们能够很好的对 Dubbo 进行拓展。Dubbo SPI 源码位于 org.apache.dubbo.common.extension 包下,后续会先介绍如何使用,再结合源码详细分析。

Dubbo 在考虑拓展点时,有一个设计概念叫做 “ 平等对待第三方 ”,也就是说框架作者能做到的功能,拓展者也一定能做到。微核心+插件式,则是比较能达到开闭原则的思路,详细介绍参考官方文档的开发者指南下的设计原则中的拓展点重构介绍。

以下示例参考自 dubbo 官方文档。


Java SPI

首先,定义一个接口,名称为 Robot。

public interface Robot {

    void sayHello();
}

接下来,定义来个实现类:BumblebeeOptimusPrime

public class Bumblebee implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

public class OptimusPrime implements Robot {
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

在不考虑 SPI 机制的情况下,要使用上述两个实现类,则只能采用硬编码的方式:通过构造函数创建对象,调用 sayHello 方法。如果现在有外部也想提供该接口的实现类供内部使用,这种就很难满足了。

现在继续考虑 SPI,在 META-INF/services 文件夹下创建一个与接口全限定名称相同的文件,即 com.duofei.spi.Robot。文件内容则为接口实现类的全限定名,如下:

com.duofei.spi.provider.Bumblebee
com.duofei.spi.provider.OptimusPrime

现在,编写测试代码:

	public void javaSPI(){
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        serviceLoader.forEach(Robot::sayHello);
    }

最终结果会成功打印两条输出语句。现在如果,我们想在外部添加实现类供内部使用,那么只需要在上述的文件中,新增内容为接口实现类的全限定名称即可。

尽管这种实现方式满足了上述需求,但仍然会带来一些问题,比如它会实例化所有的实现类,这样就不太利于资源的利用了,当然,并不仅仅是由于以上原因,Dubbo 就实现了自己的一套 SPI ,毕竟量身定做,使用起来也会方便许多。


Dubbo SPI

Dubbo SPI 要求拓展点接口必须添加 @SPI 注解,即为上述的 Robot 接口添加该注解。

关于拓展点接口实现类的描述文件,放在了 META-INF/dubbo 目录下,并且内容改为了键值对的形式,上述用例的文件内容为:

bumblebee=com.duofei.spi.provider.Bumblebee
optimusPrime=com.duofei.spi.provider.OptimusPrime

文件名称仍然使用接口的全限定名,下面是测试代码:

    public void dubboSPI(){
        ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }

首先,按需加载可以从代码中体现出来,但不仅仅如此,为了让拓展机制更加灵活好用,Dubbo 还加入了其它的一些特性,如注入拓展,自适应拓展机制,自动激活策略。


源码分析:

Dubbo SPI 的整个加载机制差不多都在 ExtensionLoader 中了,那么如何去阅读源码呢?除了上速的 getExtension 方法作为入口之外,我一般喜欢按以下步骤去做分析:

  1. 查看类的所在的包结构,从一个更高的层次去看,反而,能体会到更多的东西;
  2. 查看类的继承结构,从实现的接口和继承的类,能够感知类在整个框架中的角色;
  3. 查看类的构造函数,了解实例化该类,还需要哪些条件,从而牵引出一个链式的实现关系;
  4. 查看类的成员变量,俗话说: “巧妇难为无米之炊”,你有了怎样的数据,能够在一定程度上表明你要做怎样的事了。
  5. 查看类的结构图,看类的私有、公有方法等,这种太笼统了,所以还是从类在使用所调用的方法去入手,就像上面所讲的 getExtension

这种方式是个人的一些总结,当然,还有看文档这是肯定的了。

该类并没有实现任何接口或者继承任何类,说明 SPI 的使用直接使用该类即可。该类的构造函数是私有的:

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

这里的 type 指的是拓展接口的 Class 对象。而 objectFactory 暂时看不明白,可以先搁置一边。既然构造函数是私有的,那么就一定有一个公共的静态方法来获取本身的一个对象实例,自然就找到了用例中使用的 getExtensionLoader 方法:

	public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        }
        // type 必须为接口
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        }
        // type必须添加 SPI 注解
        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;
    }

上述的逻辑是在获取 ExtensionLoader 时,会先从缓存中去读取,在不存在的情况,才重新情况下。EXTENSION_LOADERS 作为成员变量,提供了缓存已经加载的 ExtensionLoader 实例,所以它会是一个静态的,被所有实例所共享。

接着查看用例中的 getExtension(String) 方法:

	public T getExtension(String name) {
        if (name == null || name.length() == 0) {
            throw new IllegalArgumentException("Extension name == null");
        }
      	// 从这里可以看出,拓展点描述文件的应该避免设置为true
        if ("true".equals(name)) {
            return getDefaultExtension();
        }
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<Object>());
            holder = cachedInstances.get(name);
        }
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    // 根据key尝试创建实例,...接下来会展开介绍》
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

上述实例的获取也会尝试先从缓存加载,缓存不存在的情况下,通过双重检测锁才去真正地实例化拓展点实现类。

双重检测外一层是为了解决效率问题,避免大量线程竞争锁,内一层才是为了真正解决并发所带来的问题。

展开介绍 createExtension(name) 方法:

    private T createExtension(String name) {
        // 根据key获取value的 class 对象,...接下来展开介绍》
        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);
            // 将拓展对象包裹在相应的 Wrapper对象中
            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
            if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
                for (Class<?> wrapperClass : wrapperClasses) {
                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
                }
            }
            return instance;
        } catch (Throwable t) {
            throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                    type + ")  could not be instantiated: " + t.getMessage(), t);
        }
    }

getExtensionClasses() 方法将负责将指定的拓展点描述文件 key-value 键值对转换为 Map<String, Class<?>> ,存储在实例的成员变量 cachedClasses 中,并且在该方法的调用链中还初始化了 cachedWrapperClasses 成员变量。该成员变量为集合,用于将指定对象包裹在相应的 Wrapper 对象中,这在后续的 注入拓展 中会详细描述。

将拓展对象包裹在相应的 Wrapper对象中,并将 Wrapper 对象返回,使用的场景是当前有拓展点 A,其接口实现中有 B、C、D,其中 B、C 的构造函数含有参数 A 类型,那么 B、C 将作为 Wrapper 对象,在获取 D 时,最终返回的会是 C 的实现类,但 C 包装了 B (即通过构造函数传入),B 包装了 D。 需要注意的是带有构造函数含有参数 A 类型的 B、C 没法在单独创建,即调用 createExtension(name) 会抛出异常。


@SPI

该注解只有一个 value 属性值,该值代表默认拓展名,该拓展名用于ExtensionLoader 实例对象的 getDefaultExtension 方法。


@Adaptive

该注解是自适应拓展机制,它的实现比较复杂,这里只介绍其使用方式。

在 Robot 接口新增如下内容:

    @Adaptive("key")
    void showColor(URL url);

BumblebeeOptimusPrime 分别实现该方法如下:

	@Override
    public void showColor(URL url) {
        System.out.println("Hello, I an yellow.");
    }

	@Override
    public void showColor(URL url) {
        System.out.println("Hello, I am blue and white.");
    }

测试代码如下:

		ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot robot = extensionLoader.getAdaptiveExtension();
        robot.showColor(URL.valueOf("dubbo://127.0.0.1:9092?key=bumblebee"));
        robot.showColor(URL.valueOf("dubbo://127.0.0.1:9092?key=optimusPrime"));

采用这种方式,能够通过参数去决定使用哪个拓展点的实现类。


@Activate

该注释对于根据给定的条件自动激活某些扩展非常有用。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
    /**
     * 激活当前的 extension ,当 group 数组中的某个值得到匹配时
     */
    String[] group() default {};

    /**
     * 激活当前的 extension,当 URL 参数中,出现了 value 中声明的 key 时
     */
    String[] value() default {};

    /**
     * 排序
     */
    int order() default 0;
}

用例:为 OptimusPrime 实现类添加 @Activate(value = "key", group = "group") 注解,测试代码如下:

		ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        List<Robot> activateExtension =
                extensionLoader.getActivateExtension(URL.valueOf("dubbo://127.0.0.1:9092?key=optimusPrime"), "key", "group");
        activateExtension.forEach(Robot::sayHello);

尽管 getActivateExtension 提供了几个重载方法,但最终实现都在 getActivateExtension(URL url, String[] values, String group) 中:

	public List<T> getActivateExtension(URL url, String[] values, String group) {
        List<T> exts = new ArrayList<T>();
        List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
        // keys 是否需要剔除默认激活的,即 values包含了 "-default" 
        if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
            getExtensionClasses();
            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;
                }
                // 匹配组
                if (isMatchGroup(group, activateGroup)) {
                    T ext = getExtension(name);
                    // !names.contains(name) 的判断,避免重复加载了 values 中指定的拓展名;
                    if (!names.contains(name)
                            && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
                            && isActive(activateValue, url)) {
                        exts.add(ext);
                    }
                }
            }
            Collections.sort(exts, ActivateComparator.COMPARATOR);
        }
        // 从拓展名称中加载
        List<T> usrs = new ArrayList<T>();
        for (int i = 0; i < names.size(); i++) {
            String name = names.get(i);
            if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
                    && !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
                if (Constants.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;
    }

上面的获取激活拓展涉及三个参数 ,分别是URL,values 拓展名,以及指定的组名;具体的匹配策略可按如下区分:

  • values 拓展名不包含剔除默认字符串("-default"):加载 Activate 注解的拓展类;

    • 指定组名匹配 Activate 注解的 group;
      • Activate 注解的value 值匹配 URL 中的key;
  • 加载 values 指定的拓展实现类

从以上大致可以判断,dubbo 将带有 Activate 注解的拓展当做 default ,我们可以在调用 getActivateExtension 方法时,在指定拓展点名称时,在里面包含 -default ,即可剔除默认的拓展实现;


依赖注入

在通过 createExtension 创建拓展点的时候,有这样一个 injectExtension(instance) 方法调用,其目的是为了注入拓展:

	private T injectExtension(T instance) {
        try {
            if (objectFactory != null) {
                // 反射获取实例所有方法,遍历方法列表
                for (Method method : instance.getClass().getMethods()) {
                    // 检测方法名是否具有 setter 方法特征
                    if (method.getName().startsWith("set")
                            && method.getParameterTypes().length == 1
                            && Modifier.isPublic(method.getModifiers())) {
                        /**
                         * 使用 DisableInject 注解,禁止依赖注入
                         */
                        if (method.getAnnotation(DisableInject.class) != null) {
                            continue;
                        }
                        Class<?> pt = method.getParameterTypes()[0];
                        if (ReflectUtils.isPrimitives(pt)) {
                            continue;
                        }
                        try {
                            String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                            Object object = objectFactory.getExtension(pt, property);
                            if (object != null) {
                                // 反射调用 setter 方法,将依赖设置到目标对象中
                                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;
    }

这是 Dubbo IOC,其注入实现是将拓展实现类中带有 setter 方法特征的属性注入。需要注入的依赖属性来自 objectFactory 对象,查找该对象的实例化处,发现它是在构造函数中实例化的:

objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());

这里已经在利用 SPI 的拓展特性,查看 ExtensionFactory 接口,其带有 @SPI 注解,查看其实现类,分别是 AdaptiveExtensionFactorySpiExtensionFactorySpringExtensionFactoryAdaptiveExtensionFactory 用于创建自适应的拓展。SpiExtensionFactory则是用于从 dubbo SPI 中获取所需要的拓展, SpringExtensionFactory是用于从 Spring 的 IOC 容器中获取所需的拓展。

在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactoryAdaptiveExtensionFactory内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory


总结

dubbo 自己实现了 SPI 机制,相比 Java 不灵活的加载方式,它大概提供了以下几个功能:

  1. 由于拓展实现类描述文件内容改写为 key-value 的形式,所以可以实现按自定义key 获取拓展实现类

  2. 由于要求拓展点必须添加 @SPI 注解,而该注解又支持默认拓展实现类的指定,所以可以实现指定默认拓展实现类

  3. 之前在创建 createExtension 中,有一个关于 cachedWrapperClasses 的操作,其实这是一种包装,提供针对拓展点实现的一种包装,有点类似于一种链。

  4. 关于 @Adaptive 注解,这个只有在通过 getAdaptiveExtension 获取到的拓展时,才会有效;并且限制了实现类中只有一个类能够添加该注解;如果要将该注解添加在方法上,只能在拓展点的接口上添加

    在调用 getAdaptiveExtension 时遵循以下逻辑:

    1. 实现类中有一个类有该注解,返回该实现类,注解在拓展点方法上的 @Adaptive 不生效;
    2. 实现类中没有该注解,拓展点接口上的某些方法存在该注解,那么将通过代码构建代理类的内容,并通过 "javassist" 编译该对象生成 Class 对象(这里也是利用了 SPI 的,具体可查看源码),并在调用带有 @Adaptive 方法时,再去选择具体的实现类,如果调用了未注解的方法,则会得到一个异常;
    3. 实现类中没有该注解,拓展点接口上的方法也没有该注解,抛出异常;
  5. 关于 @Activate 注解,在调用 getActivateExtension 时生效;其属性 values 针对方法参数 URL 中是否存在 key,属性 group 针对方法参数 group,order 属性则用于排序,并且添加了该注解的实现类会在调用该方法时,作为 default 实现类,可以通过在方法参数中,添加 “-default” 排除。

其实对于各注解的使用不要混淆,它们只有在调用对应方法时,才会生效;比如说你在调用 getExtension 方法时, @Activate 注解并不会生效。

可以说,对于 SPI 实现,dubbo 还是做了很多工作,重点关注 ExtensionLoadergetExtensiongetDefaultExtensiongetAdaptiveExtensiongetActivateExtension 几个方法。


我与风来


认认真真学习,做思想的产出者,而不是文字的搬运工
错误之处,还望指出

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值