【Dubbo系列】无处不在的SPI之羽翼渐丰

5 篇文章 0 订阅

【Dubbo系列】无处不在的SPI之崭露头角 一文介绍了JDK的SPI机制。SPI与API的区别主要在于SPI是给扩展者使用的,目的为了扩展组件,其接口通常在调用方中;而API接口通常在实现方中,JAVA documentation中对SPI的解释:

服务通常为接口或者抽象类服务提供者提供具体的实现。服务提供者可以通过java框架提供的方式或者其他的扩展形式被安装到实现场景中,比如 在扩展路径中添加服务提供者的jar包。服务提供者可以通过应用的class path或者其他框架特定形式被添加到应用中。

原文:A service is a well-known set of interfaces and (usually abstract) classes. A service provider is a specific implementation of a service. The classes in a provider typically implement the interfaces and subclass the classes defined in the service itself. Service providers can be installed in an implementation of the Java platform in the form of extensions, that is, jar files placed into any of the usual extension directories. Providers can also be made available by adding them to the application's class path or by some other platform-specific means.

前文使用ServiceLoader实现SPI有以下缺陷:

  • 需要调用服务时必须加载所有的扩展类并生成对象,导致效率低下。
  • 扩展类没有标志可以进行有效区分。当调用者需要调用某一个扩展类时无法有效指定。

Dubbo中对ServiceLoader进行改写,来解决上述的缺陷:

  1. 建立注解用来表明为SPI接口,其中value值为默认扩展类标识:
package com.ryan;

import java.lang.annotation.*;

// 判断是否为SPI接口注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {

    /**
     * default extension name
     */

    String value() default "";

}
  1. 使用ExtensionLoader代替ServiceLoader获取扩展类对象,希望通过以下方法来获取扩展类:
ExtensionLoader<Protocol> extensionLoader = new ExtensionLoader<> (Protocol.class);
Protocol dubbo = extensionLoader.getExtension (Protocol.class, "dubbo");
dubbo.invoke ();
  1. 需要在配置中添加前缀表示扩展类的属性,比如com.ryan.myspi.DubboProtocol的扩展类属性`name``dubbo`
dubbo=com.ryan.myspi.DubboProtocol
hession=com.ryan.myspi.HessianProtocol

设置该配置路径在META-INF/services/路径下。

  • 需要建立两个缓存,一个缓存类对象,一个缓存实例对象
// final关键字修饰,表明对象实例不可以被修改
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>> ();
// 存放class实现类的缓存
private final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object> ();
  • 需要对配置进行解析,并且根据参数name加载对应的扩展类全限定名。

方法调用过程如下,省略了部分细节:

1544839947417
1544839947417
1544839947417

其中getExtension方法获取扩展类对象:

public T getExtension(Class<T> type,String name) {
    // 判断接口是否有SPI注解
    if(!withExtensionAnnotation (type)){
        throw new IllegalArgumentException (" type WITHOUT @" + SPI.class.getSimpleName () + " Annotation ");
    }
    // 参数校验
    if (name == null || name == "") {
        throw new IllegalArgumentException (" getExtension params name == null ");
    }
    try {
        // 获取扩展类对象,该对象作为实例对象缓存的key值
        Class<?> clazz = getExtensionClasses(type).get(name);
        // 查缓存
        T instance = (T) EXTENSION_INSTANCES.get (clazz);
        if(instance==null){
            EXTENSION_INSTANCES.putIfAbsent (clazz, clazz.newInstance ());
            instance = (T) EXTENSION_INSTANCES.get (clazz);
        }
        return instance;
    } catch (Throwable e) {
        throw new IllegalArgumentException (" clazz " + type + " newInstance error");
    }
}

getExtension时需要获取对应的类对象,调用方法getExtensionClasses

private Map<String, Class<?>> getExtensionClasses(Class<T> type) {

    Map<String, Class<?>> classes = cachedClasses.get ();
    // dubbo-check
    if (classes == null) {
        // 加锁,防止重复创建
        synchronized (cachedClasses) {
            classes = cachedClasses.get ();
            if(classes==null) {
                classes = loadExtensionClasses (type);
            }
            cachedClasses.set (classes);
        }
    }
    return classes;
}

该方法中返回Map对象,key值为扩展类namevalue值为扩展类对象,先查找cachedClasses是否存在该扩展类对象集合,如果不存在需要进行加载配置:

private Map<String, Class<?>> loadExtensionClasses(Class<T> type) {
    // 为后续代码维护者提供一个信息,该注解的对象引用不可被改变
    final SPI defaultAnnotation = type.getAnnotation (SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value ();
        defaultName = value;
    }
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>> ();

    // fileName = 根路径+接口全限定名
    String fileName = "META-INF/services/" + type.getName ();
    // 读取fileName里的资源

    Enumeration<URL> urls;
    ClassLoader classLoader = findClassLoader ();

    try {
        // 加载配置资源,URL表示资源位置
        urls = classLoader.getResources (fileName);
        if (urls != null) {
            while (urls.hasMoreElements ()) {
                java.net.URL resourceURL = urls.nextElement ();
                // loadResource ,加载资源,重头戏来了
                loadResource (extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable e) {
        e.printStackTrace ();
    }

    return extensionClasses;
}

其中loadResource, 读取META-INF/services/下的配置文件,并放入extensionClasses中返回

// 加载资源
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceURL) {

    try {
        BufferedReader reader = new BufferedReader (new InputStreamReader (resourceURL.openStream (), "utf-8"));
        // 读取配置
        try {
            String line;
            while ((line = reader.readLine ()) != null) {
                line = line.trim ();
                if (line.length () > 0) {
                    String name;
                    String[] split = line.split ("=");
                    name = split[0];
                    line = split[1];
                    // 加载类
                    loadClass (extensionClasses, Class.forName (line, true, classLoader), name);
                }
            }
        } finally {
            reader.close ();
        }
    } catch (Throwable e) {
        e.printStackTrace ();
    }
}

loadClass方法如下:

private void loadClass(Map<String, Class<?>> extensionClasses, Class<?> clazz, String name) {
    extensionClasses.put (name, clazz);
}

获得扩展类Class后,便可以通过clazz.newInstance ()得到具体的扩展类对象。

遗留问题

通过对JDK的SPI进行增强,可以获得单一服务的扩展类实现类,但是在Dubbo中,SPI应用于各种服务,如下截图。需要写一个工厂模式来根据不同的服务生成不同的ExtensionLoader

1544841006191
1544841006191

另外如果一个A服务中引用了B服务,那么如果确定引用的B服务的扩展实现类呢? Dubbo引进了简单的 IOC 功能来实现依赖注入。在下文中介绍对两种问题的增强。

如果需要原文代码的朋友请关注公众号,并在后台回复:spi1

推荐阅读:

Mybatis拦截器源码深度解析

线程池的前世今生

了解更多请关注微信公众号

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值