【dubbo】dubbo SPI机制(ExtensionLoader)

    Dubbo强大的扩展能力,主要依赖于它自己实现的一套SPI机制,开发人员可以根据dubbo的规范进行扩展现有的功能或者替换现有的实现,dubbo内部的功能的实现也都是通过的SPI来实现的,这是dubbo的功能高度可插拔的原因。但是dubbo并没有使用Java的SPI,之所以没有使用Java自带的SPI在官方文档上有如下阐述。

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

    如果想要使用Java自带的SPI,可以参考java SPI (Service Provider Interface)

使用约定

    在扩展类的jar包内,放置扩展点配置文件:META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。
注意:这里的配置文件是放在你自己的jar包内,不是dubbo本身的jar包内,Dubbo会全ClassPath扫描所有jar包内同名的这个文件,然后进行合并
dubbo SPI的主要是通过ExtensionLoader这个类来实现的,所有关键代码也都在这个类中,使用方式有两种,一种可以手工指定,另外一种是自动生成。

基本用法和示例

    有一个DuSPI的接口,接口中有一个sayHi的方法,然后通过dubbo的SPI机制进行调用其相关实现。

接口类

必须要有@SPI注解 ,SPI注解可以有一个value参数,代表默认的实现。

//接口类,定义了一个SayHello的方法,需要被实现
@SPI
public interface DuSPI {
    public String SayHello(String hi);
}

//实现类,没什么好说的。
public class LocalDuSPI implements DuSPI {
    @Override
    public String SayHello(String hi) {
        return "FROM LocalDuSPI : "+hi;
    }
}


扩展点配置

105412_J3N6_2457218.png

调用方式

public class DuSPIMain {
    private static final DuSPI duSPI = ExtensionLoader.getExtensionLoader(DuSPI.class).getExtension("local");
    public static void main(String[] args) {
        String hi = duSPI.SayHello("扩展SPI");
        System.out.println(hi);
    }
}

输出结果为

105526_EQYa_2457218.png

    上述就是非常简单的手工指定实现类用例。getExtension("local");接收一个key值,这个key就是spi文件中的key,得到的实现就是key对应的实现类。

    ExtensionLoader还有一种自动寻找扩展的方法,getAdaptiveExtension,不需要手工指定实现,而是在方法具体调用的时候自动发现其具体的实现类。例如Protocol扩展,它的export方法就是通过invoke的URL中的Protocol,自动的发现实现

private static final ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();

Invoker<?> invoker = proxyFactory.getInvoker(ref, (Class) interfaceClass, registryURL.addParameterAndEncoded(Constants.EXPORT_KEY, url.toFullString()));

Exporter<?> exporter = protocol.export(invoker);

Duboo SPI源码解析ExtensionLoader类

通过对类注释,可以看到这个类主要实现以下几个功能

自动注入关联扩展点

自动Wrap上扩展点的Wrap类

缺省获得的扩展点的一个内部类Adaptive

这个类的实例需简要有一个getExtensionLoader的静态工厂方法,接收一个Class类型的对象,来获取其实例。

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

    通过以上代码可以看出,传入的对象必须是一个接口,并且必须是带有@SPI注解,获取完了以后,针对相应的接口类型会缓存到map中去。

根据key返回相应的实现类方法
getExtensionClass(String name)

    首先会去在缓存里面查找,看是否已经加载过这个类,如果加载过的话就直接返回,如果没有,则配置文件中查找,然后进行加载。这样就达到了它说的jdk的spi要全部加载所有扩展,而不是按需加载,如果有相应的扩展找不到就会报错的或者一个类的加载很耗的问题。
创建扩展是使用的createExtension方法,类实例化以后,会查看有没有对其他扩展点的应用,如果有的话就调用injectExtension方法,去注入其他的扩展类,这样就达到了它所说的AOP的功能。

加载扩展类的方法是loadExtensionClasses

private Map<String, Class<?>> loadExtensionClasses() {
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if(defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if(value != null && (value = value.trim()).length() > 0) {
            String[] names = NAME_SEPARATOR.split(value);
            if(names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
                        + ": " + Arrays.toString(names));
            }
            if(names.length == 1) cachedDefaultName = names[0];
        }
    }
    
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadFile(extensionClasses, DUBBO_DIRECTORY);
    loadFile(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

通过以上可以得出以下结论

  • 加载SPI注解,注解的value只能有一个,也就是默认扩展实现类。
  • Dubbo会依次扫描META-INF/dubbo/internal/、META-INF/dubbo/、
  • META-INF/services/ 这三个文件夹,优先级正好相反,看有没有相应的SPI文件、通过loadFile方法进行扫描和加载。
     
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
        String fileName = dir + type.getName();
        try {
            ...
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    java.net.URL url = urls.nextElement();
                    try {
                        BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
                        try {
                            String line = null;
                            while ((line = reader.readLine()) != null) {
                                final int ci = line.indexOf('#');
                                if (ci >= 0) line = line.substring(0, ci);
                                line = line.trim();
                                ...
                                        int i = line.indexOf('=');
                                        if (i > 0) {
                                            name = line.substring(0, i).trim();
                                            line = line.substring(i + 1).trim();
                                        }
                                ...
                                            Class<?> clazz = Class.forName(line, true, classLoader);
                                            if (! type.isAssignableFrom(clazz)) {
                                                throw new IllegalStateException("Error when load extension class(interface: " +
                                                        type + ", class line: " + clazz.getName() + "), class "
                                                        + clazz.getName() + "is not subtype of interface.");
                                            }
                                
                                            if (clazz.isAnnotationPresent(Adaptive.class)) {
                                                if(cachedAdaptiveClass == null) {
                                                    cachedAdaptiveClass = clazz;
                                                } else if (! cachedAdaptiveClass.equals(clazz)) {
                                                    throw new IllegalStateException("More than 1 adaptive class found: "
                                                            + cachedAdaptiveClass.getClass().getName()
                                                            + ", " + clazz.getClass().getName());
                                                }
                                            } else {
                                                try {
                                                    clazz.getConstructor(type);
                                 ...
                                                    String[] names = NAME_SEPARATOR.split(name);
                                                    if (names != null && names.length > 0) {
                                                        Activate activate = clazz.getAnnotation(Activate.class);
                                                        if (activate != null) {
                                                            cachedActivates.put(names[0], activate);
                                                        }
                                ...
                                                    }
                                                }
                                            }
                                        }
                                  
    }
}
  1. 通过加载类的文件可以看出
  2. 首先会扫描目录下的所有文件,
  3. 逐行读取配置文件
  4. 忽略#号后内容作为注释
  5. 分割字符串,等号前面的作为key,等号后面的作为value
  6. 通过Class.forName(value)获取具体的实现类对象
  7. 做一些类的类型校验
  8. 缓存得到的类

自适应Extention(Protocol示例)

在生成对象引用的时候,并不直接生成实现对象,而是先生成一个代理对象,直到调用的时候,在根据传递的参数决定调用的是哪个具体的实现类。Dubbo的SPI实现了自动发现机制。通过调用getAdaptiveExtension方法生成代理对象。在Protocol接口的使用中就用到了这个方法。
ServiceConfig类用需要有引用对象Protocol,这是标明dubbo的服务注册所使用的协议。Protocol的实现类很多
110034_XqgT_2457218.png

但是在声明的时候并不指名要调用哪种协议的实现。

private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();

代码上调用了getAdaptiveExtension()方法,生成代理对象,由于是代码生成的代理对象,在dubbo中并没有定义,所以只能把类文件拼装的代码的代码打印出来,生成类文件的代码是ExtensionLoader的createAdaptiveExtensionClassCode方法。生成的代理对象的类的定义如下:

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;

public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
    public void destroy() {
        throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public int getDefaultPort() {
        throw new UnsupportedOperationException("method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
    }

    public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws java.lang.Class {
        if (arg1 == null) throw new IllegalArgumentException("url == null");
        com.alibaba.dubbo.common.URL url = arg1;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.refer(arg0, arg1);
    }

    public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0) throws com.alibaba.dubbo.rpc.Invoker {
        if (arg0 == null) throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
        if (arg0.getUrl() == null)
            throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
        com.alibaba.dubbo.common.URL url = arg0.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null)
            throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
        com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol) ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
        return extension.export(arg0);
    }
}

这是一个通过javassit自动生成的一个类,实现了Protocol接口,对于服务端内容导出发布,实现了export方法,export 方法中,首先获取Invoker的url,URL中获取Protocol的protocol属性的值,看是否进行了设置,默认为dubbo,然后再调用dubboSPI(ExtensionLoader)机制中的getExtension方法,把Key传入进去,以达到自动获取Protocol的目的,然后找到真正的实现类,再调用其export方法。如果采用zk作为注册中心,通过配置<dubbo:registry address="zookeeper://localhost:2181" />
这样取得的key是就是registry,真正的实现类则是

com.alibaba.dubbo.registry.integration.RegistryProtocol

当然,上述只是讲述了dubbo spi设计的一个大概的思路,还有很多的其他的细节问题在本文中并未提到,不过这对于了解dubbo自定义实现SPI的设计已经足够。
 

转载于:https://my.oschina.net/u/2457218/blog/1517866

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值