Dubbo源码分析——扩展点机制

1、概述

​ dubbo中定义了很多的扩展点,用SPI注解声明的接口就是一个扩展点。扩展点的每一个实现称为extension。dubbo使用ExtensionLoader来加载所有的Extension,ExtensionLoader的作用和spring容器有点像,spring容器负责创建和管理所有配置的bean,而ExtensionLoader负责加载所有的扩展点实现,不仅如此,ExtensionLoader也支持对扩展点进行依赖注入(后面会讲到),还支持扩展点的Wrapper机制。

​ 我们都知道JDK也提供了SPI机制,那么为什么dubbo还要实现一套扩展点机制呢?相比JDK标准的SPI,dubbo的扩展点机制在以下几个方面进行了改进:

  • JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源,而dubbo扩展点机制实现了按需加载,只有在明确请求获取一个SPI的时候才创建相应的扩展点实现。
  • JDK标准的SPI如果扩展点加载失败,连扩展点的名称都拿不到了。比如:ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。dubbo扩展点对这种情况会给出明确的失败原因,让排查问题更容易。
  • JDK标准的SPI扩展点是独立的,彼此之间没有建立起关联关系。而dubbo扩展点机制增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。

2、扩展点配置方式

在jar包的META-INF/dubbo/,META-INF/dubbo/internal/和META-INF/services/,这三个目录下放置SPI接口全限定名作为为文件名的文件,文件的内容为:配置名=扩展实现类全限定名,多个扩展点实现用换行分隔。举个例子,Protocol扩展点的配置文件位于dubbo-rpc这个jar包的META-INF/dubbo/internal/目录下,文件名是com.alibaba.dubbo.rpc.Protocol,内容如下:

dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol

当然这个文件是在dubbo自己的jar包下的,我们当然可以自己写一个类实现com.alibaba.dubbo.rpc.Protocol,然后在自己的jar包的/和META-INF/services/或者META-INF/dubbo/目录下放置一个名为com.alibaba.dubbo.rpc.Protocol的文件,把实现类的名字作为文件内容,dubbo也会加载我们的这个扩展点实现。事实上dubbo会扫描所有classpath下的SPI配置文件,将相同文件名的内容进行合并,平等地对待预定义和外部提供的扩展点实现。

需要注意的是,ExtensionLoader会cache扩展点的实现,因此所有扩展点的实现类都必须是线程安全的,作为单例提供服务。

3、扩展点的自动包装

​ 如果一个SPI接口有多个实现,这时候需要在每一个实现的前后都加一段逻辑(比如统计rt),该怎么办呢,要去改每一个实现类的代码吗?如果实现类第三方jar包中的怎么办?关于这点,dubbo作者早就想到了,他们提供了扩展点的Wrapper机制。wrapper其实也是一个扩展点实现,它和被wrapper的扩展点实现一样,都实现了相同的SPI接口,不同的是wrapper有一个拷贝构造函数。举个例子,dubbo-rpc-api这个jar包中配置了如下两个扩展点实现:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper

那么当获取名为dubbo的这个扩展点实现单例的时候,ExtensionLoader首先会创建DubboProtocol对象,然后用

ProtocolFilterWrapper和ProtocolListenerWrapper对其进行封装,返回封装之后的对象,实际进行调用的时候,调用顺序是这样的:

ProtocolListenerWrapper => ProtocolFilterWrapper => DubboProtocol

dubbo对扩展点实现进行封装的代码如下:

if (wrapperClasses != null && wrapperClasses.size() > 0) {
    for (Class wrapperClass : wrapperClasses) {
        instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
    }
}
return instance;

dubbo以这种形式实现了扩展点的代理和AOP机制。

4、扩展点的自动注入

前面说dubbo的ExtensionLoader有点类似spring容器,是一个专门管理扩展点的容器,那么依赖注入功能肯定是少不了的,不然如果不同的扩展点有相互依赖的,还得自己拿到一个个扩展点,去手动注入,太麻烦了。dubbo也替我们想到了这一点,它能够对扩展点实现进行依赖注入。其实现原理很简单,代码如下:

private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {//ExtensionFactory自己不需要这个逻辑
            for (Method method : instance.getClass().getMethods()) {
                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) : "";
                        //FIXME  通过SpiExtensionFactory获取adaptiveExtension  add by jileng
                        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;
}

一句话概括就是遍历set方法,获取参数的类型和名字,然后以此在容器中寻找这样的扩展点。

不过具体的寻找依赖的工作是由objectFactory成员完成的,它的声明如下

private final ExtensionFactory objectFactory;

ExtensionFactory也是一个扩展点:

@SPI
public interface ExtensionFactory {

    /**
     * Get extension.
     * 
     * @param type object type.
     * @param name object name.
     * @return object instance.
     */
     T getExtension(Class type, String name);

}

那ExtensionFactory是怎么寻找依赖的呢?其实ExtensionFactory有两个扩展点实现SpiExtensionFactory和SpringExtensionFactory。SpiExtensionFactory负责寻找名称是name类型是type的扩展点实现,而SpringExtensionFactory负责在spring容器中寻找名称为name的bean。而AdaptiveExtensionFactory是ExtensionFactory的Adaptive实现,它负责协调其它实现扩展点实现来完成工作。下面会介绍Adaptive扩展点实现的作用。

五、扩展点自适应

上文说了,SpiExtensionFactory会寻找扩展点实现作为依赖注入的对象,但是如果扩展点有多个实现,那么该选哪一个实现呢?换个方式提问,既然有多个扩展点实现,那么在引用扩展点的时候怎么决定用哪一个实现呢?如果写死的话,也就失去了扩展点提供的灵活性。dubbo使用Adaptive扩展点解决这个问题。

下面的代码是SpiExtensionFactory寻找依赖的逻辑:

public class SpiExtensionFactory implements ExtensionFactory {

    public  T getExtension(Class type, String name) {
        if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
            ExtensionLoader loader = ExtensionLoader.getExtensionLoader(type);
            if (loader.getSupportedExtensions().size() > 0) {
                //返回Adaptive Extension
                return loader.getAdaptiveExtension();
            }
        }
        return null;
    }

}

可见ExtensionLoader注入的依赖扩展点是一个Adaptive实例。Adaptive实例也是一个扩展点实现,但是它本身不干活,它的作用是根据URL(Dubbo使用URL对象传递配置信息)动态地决定由哪个具体的扩展点实现来干活,然后把工作代理给真正干活的实现。Adaptive扩展点可以用@Adaptive注解来显式声明,每一个扩展点最多只能有一个显式声明的Adaptive扩展点。如果没有显式声明Adaptive扩展点怎么办呢?答案是动态生成一个!如下面的代码所示:

private Class createAdaptiveExtensionClass() {
    String code = createAdaptiveExtensionClassCode();
    ClassLoader classLoader = findClassLoader();
    com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
    return compiler.compile(code, classLoader);
}

其中createAdaptiveExtensionClassCode方法是用来生成源代码的,实现代码比较复杂,但是逻辑却很简单,总结起来就是:为扩展点接口的每一个带有@Adaptive注解的方法生成一个方法体,方法体的逻辑是找到参数中类型是URL的对象或者参数中的URL成员,然后根据方法@Adaptive注解上声明的key,去url对象里面找对应的value,这个value就是真正干活的扩展点实现的名称,然后从ExtensionLoader中获取这个扩展点实现,把工作代理给它。下面是一个例子,便于理解:

public interface Transporter {
    @Adaptive({"server", "transport"})
    Server bind(URL url, ChannelHandler handler) throws RemotingException;

    @Adaptive({"client", "transport"})
    Client connect(URL url, ChannelHandler handler) throws RemotingException;

}

对于bind方法表示,Adaptive实现先查找”server”key,如果该Key没有值则找”transport”key值,来决定代理到哪个实际扩展点。

六、总结

​ dubbo其实是一个微内核的架构,很多功能都是通过扩展点来实现的,业务方可以方便的添加自己想要的功能,比如打印rpc日志、上报rt监控等。我们自己在写代码尤其是框架性的代码时,可以借鉴下dubbo的思路。

你可能感兴趣的:(源码系列,dubbo)