前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一、dubbo初体验之SPI

一、dubbo初体验之SPI

作者头像
JathonKatu
发布2022-12-02 19:35:45
4510
发布2022-12-02 19:35:45
举报
文章被收录于专栏:JathonKatu

什么是SPI

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。

了解过springboot的同学或许大多都认识一个文件:spring.factories

他存在的意义,就是为了告诉spring容器,某一个接口,存在哪些接口,他的哪些实现类是我们所需要的,动态替换接口的。

同样的,java也有一套SPI的机制。

java的SPI主要依靠的是 ServiceLoader ,而spring的SPI主要依靠的是 SpringFactoriesLoader ,而dubbo的SPI主要依靠的是 ExtensionLoader。

java的SPI我们这里就不另加描述了,可以看https://dubbo.apache.org/zh/docsv2.7/dev/source/dubbo-spi/

这里我们从ExtensionLoader切入dubbo的第一课源码。

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

映入眼帘的,除了Logger之外,就是三个加载路径。跟java的SPI一样,是从规定路径下,读取类的全限定名作为加载的依据。加载的优先顺序从下至上。

代码语言:javascript
复制
private static final String SERVICES_DIRECTORY = "META-INF/services/";

private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";

接下来,就是一个默认加载默认扩展类名称的校验器(,左右无限个空格的正则表达式),加载扩展类的加载类的缓存map,和扩展类的缓存map。跟spring的三级缓存有些类似。通过EXTENSION_LOADERS的value(loader)获取到EXTENSION_INSTANCES里的对象(实际的扩展类)。

代码语言:javascript
复制
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");

private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();

private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();

以上就是这个类的所有静态常量类。

在进入接下来的解析之前我们不妨看一个简单的demo:

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。

在上面涉及到的三个加载路径下任意一个路径下配置文件,文件名称是Robot的全限定名,而optmusPrime和bumblebee是Robot的实现类。

代码语言:javascript
复制
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

接着我们进行测试:

代码语言:javascript
复制
public class JavaSPITest {

    @Test
    public void sayHello() throws Exception {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Robot::sayHello);
    }
}

会输出optimusPrime和bumblebee的sayHello方法的结果。

我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。

接下来我们去解析《ExtensionLoader》对象的每个属性。这些属性是笔者在阅读各个方法的时候自己推测出来的,所以光看可能结果会很懵逼,需要结合方法去解读。

代码语言:javascript
复制
private final Class<?> type; // 这个loader类实际要loader的接口类型

private final ExtensionFactory objectFactory; // 用于获取自适应类的工厂

private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>(); // 用于保存class, name的键值对,一个class只会保存一次

private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>(); // 用于保存name , class的键值对,并且一个name只能对应一个class,否则报错

private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>(); // 保存激活器对应的name和激活器本身(激活器类似于一个注解,实现aop)

private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();  // 用于保存name和holder的键值对,holder用于保存扩展类

private final Holder<Object> cachedAdaptiveInstance = new Holder<Object>(); // 自使用扩展类的实例

private volatile Class<?> cachedAdaptiveClass = null; // 自适应扩展类的存储

private String cachedDefaultName; // @SPI的value,用来保存默认的扩展类的名字

private volatile Throwable createAdaptiveInstanceError;

private Set<Class<?>> cachedWrapperClasses;

private Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<String, IllegalStateException>();  // 用于存储异常信息,根据配置文件中的"key = value"获取对应的异常信息

下面我们从 ExtensionLoader 的 getExtension 方法作为入口,对拓展类对象的获取过程进行详细的分析。

getExtension方法很简单,首先检查缓存,如果缓存未命中则创建拓展对象。

代码语言:javascript
复制
public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }
    // Holder,顾名思义,用于持有目标对象
    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) {
                // 创建拓展实例
                instance = createExtension(name);
                // 设置实例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

接下来我们解析创建拓展对象的过程

代码语言:javascript
复制
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);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循环创建 Wrapper 实例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
                // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
                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);
    }
}

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

1.通过 getExtensionClasses 获取所有的拓展类

2.通过反射创建拓展对象

3.向拓展对象中注入依赖

4.将拓展对象包裹在相应的 Wrapper 对象中

步骤1是加载拓展类的关键,步骤3,4是dubbo 的 ioc 和 aop的表现

下面我们来重点分析getExtensionClasses 方法的逻辑,以及简单介绍 Dubbo IOC 的具体实现。

代码语言:javascript
复制
private Map<String, Class<?>> getExtensionClasses() {
    // 从缓存中获取已加载的拓展类
    Map<String, Class<?>> classes = cachedClasses.get();
    // 双重检查
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加载拓展类
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

这里也是先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类。下面分析 loadExtensionClasses 方法的逻辑。

代码语言:javascript
复制
private Map<String, Class<?>> loadExtensionClasses() {
    // 这里的type 在ExtensionLoader的构造函数就已经传进来了
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        // 获取SPI注解配置的默认拓展类别名
        String value = defaultAnnotation.value();
        if ((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));
            }
            // 如果只有一个默认扩展类的别名,则缓存的默认名称为这个值(参考getDefaultExtension)
            if (names.length == 1) cachedDefaultName = names[0];
        }
    }

    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // 加载指定路径下的文件
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI的解析也就是看看是否存在默认拓展类的别名,如果存在并且合法,则缓存默认别名。

接下来我们来解析loadDirectory方法。

代码语言:javascript
复制
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件路径 + 类的全限定名称
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        // 找到ExtensionLoader类的classloader
        ClassLoader classLoader = findClassLoader();
        // 找得到classLoader则用它来加载所有的同名配置文件
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
            // 找不到则用默认的APPClassLoader
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        // 配置文件中的
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // 加载资源
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class(interface: " +
                type + ", description file: " + fileName + ").", t);
    }
}

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。

我们继续解析loadResource

代码语言:javascript
复制
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // 按行读取配置文件
            while ((line = reader.readLine()) != null) {
                // 定位 # 字符
                final int ci = line.indexOf('#');
                // 截取 # 之前的字符串,# 之后的内容为注释,需要忽略
                if (ci >= 0) line = line.substring(0, ci);
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // 以等于号 = 为界,截取键与值 (也就是别名与扩展类)
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加载类,并通过 loadClass 方法对类进行缓存
                            loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
                        exceptions.put(line, e);
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class(interface: " +
                type + ", class file: " + resourceURL + ") in " + resourceURL, t);
    }
}

loadResource用于解析配置文件,#是注释,通过 = 号划分别名和类全限定名。通过反射加载类,最后调用loadClass方法进行其他操作。loadClass方法主要用于缓存,

接下来我们来解析loadClass方法

代码语言:javascript
复制
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    // 如果这个类不是type的实现类,则报错
    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.");
    }
    // 如果这个类是有Adaptive的注解  用于默认加载扩展类
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        // 判断是否已经存在Adaptive扩展类,如果不存在缓存起来,如果存在且不是本类则报错
        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());
        }
    // 检查类是否是wrapper类型  wrapper类型的构造函数参数只有type本身(也就是这个钩子的接口)
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        // 如果cachedWrapperClasses为空则初始化
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        // 存储 clazz 到 cachedWrapperClasses 缓存中
        wrappers.add(clazz);
    // 进入此分支说明clazz是一个普通的扩展类
    } else {
        // 检查clazz是有默认的构造方法,如果没有则抛异常
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // 如果name为空,则尝试从注解Extension中获取,或者使用小写的类名
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
            }
        }
        // 根据 ","分割
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            // 如果类上有 Activate 注解,则使用 names 数组的第一个元素作为键,
            // 存储 name 到 Activate 注解对象的映射关系
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存储Class 到名称的映射关系
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    // 存储名称到 Class 的映射关系
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
                }
            }
        }
    }
}

loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该方法没有其他什么逻辑了。

到此,关于缓存类加载的过程就分析完了。整个过程没什么特别复杂的地方,接下来我们来聊一聊dubbo IOC

代码语言:javascript
复制
private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            // 遍历实例目标类的所有方法
            for (Method method : instance.getClass().getMethods()) {
                // 检测set方法,且仅有一个参数,且为public的方法
                if (method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers())) {
                    /**
                     * 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];
                    try {
                        // 获取到set方法相应的属性名
                        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        // 从 ObjectFactory 中获取依赖对象
                        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;
}

在上面代码中,objectFactory 变量的类型为 AdaptiveExtensionFactory,AdaptiveExtensionFactory 内部维护了一个 ExtensionFactory 列表,用于存储其他类型的 ExtensionFactory。Dubbo 目前提供了两种 ExtensionFactory,分别是 SpiExtensionFactory 和 SpringExtensionFactory。前者用于创建自适应的拓展,后者是用于从 Spring 的 IOC 容器中获取所需的拓展。

在2.x的代码中,ioc只支持set方法注入。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 JathonKatu 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档