Dubbo源码浅析之SPI机制

前言

Dubbo通过ExtensionLoader实现了SPI机制,给整个框架提供了扩展性,看源码过程中发现用了很多次synchronized以及ConcurrentHashMap,粗略思考了原因,记录下这篇笔记

使用

简单说一下使用

Serialization serialization = ExtensionLoader.getExtensionLoader(Serialization.class).getExtension("kryo");

上述代码加载了下述路径下对应文件中由kryo指向的com.alibaba.dubbo.common.serialize.kryo.KryoSerialization
在这里插入图片描述
在这里插入图片描述
如果要自定义,则自己实现一个接口,并将实现类放入上述相似路径的对应文件中即可,从而实现对每个接口实现类的可拔插扩展,说白了本质就还是一个工厂模式,与IOC一样

源码浅析

流程梳理

源码部分主要就是看getExtensionLoader与getExtension两个方法了,Dubbo该机制还能够自适应地导入实现类,对应方法为getAdaptiveExtension,这部分以后有空再看了

首先,将今天要解析的部分抽离出来,可以实现最基本的拔插功能,这篇文章重点主要在于线程安全问题的分析,因为整个流程代码比较易懂,但为什么要上锁不太容易理解


import lombok.extern.slf4j.Slf4j;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static java.nio.charset.StandardCharsets.UTF_8;

/**
 * refer to dubbo spi: https://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html
 */
@Slf4j
public final class ExtensionLoader<T> {

    private static final String SERVICE_DIRECTORY = "META-INF/extensions/";
    private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
    private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();

    private final Class<?> type;
    private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

    private ExtensionLoader(Class<?> type) {
        this.type = type;
    }

    public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type should not be null.");
        }
        if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type must be an interface.");
        }
        if (type.getAnnotation(SPI.class) == null) {
            throw new IllegalArgumentException("Extension type must be annotated by @SPI");
        }
        // firstly get from cache, if not hit, create one
        ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
        if (extensionLoader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));
            extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
        }
        return extensionLoader;
    }

    public T getExtension(String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Extension name should not be null or empty.");
        }
        // firstly get from cache, if not hit, create one
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        }
        // create a singleton if no instance exists
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

    private T createExtension(String name) {
        // load all extension classes of type T from file and get specific one by name
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw new RuntimeException("No such extension of name " + name);
        }
        // 根据类型从全局获取
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            try {
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            } catch (Exception e) {
                log.error(e.getMessage());
            }
        }
        return instance;
    }

    private Map<String, Class<?>> getExtensionClasses() {
        // get the loaded extension class from the cache
        Map<String, Class<?>> classes = cachedClasses.get();
        // double check
        // 这里加锁是避免重复类加载,必须保证每个类全局只加载一次
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = new HashMap<>();
                    // load all extensions from our extensions directory
                    loadDirectory(classes);
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }

    private void loadDirectory(Map<String, Class<?>> extensionClasses) {
        String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
        try {
            Enumeration<URL> urls;
            ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            urls = classLoader.getResources(fileName);
            if (urls != null) {
                while (urls.hasMoreElements()) {
                    URL resourceUrl = urls.nextElement();
                    loadResource(extensionClasses, classLoader, resourceUrl);
                }
            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {
            String line;
            // read every line
            while ((line = reader.readLine()) != null) {
                // get index of comment
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // string after # is comment so we ignore it
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        final int ei = line.indexOf('=');
                        String name = line.substring(0, ei).trim();
                        String clazzName = line.substring(ei + 1).trim();
                        // our SPI use key-value pair so both of them must not be empty
                        if (name.length() > 0 && clazzName.length() > 0) {
                            Class<?> clazz = classLoader.loadClass(clazzName);
                            extensionClasses.put(name, clazz);
                        }
                    } catch (ClassNotFoundException e) {
                        log.error(e.getMessage());
                    }
                }

            }
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

其中包括两个静态变量,可以理解为全局变量

  • EXTENSION_LOADERS
  • EXTENSION_INSTANCES

两个成员变量

  • cachedInstances
  • cachedClasses

假设现在有interfaceA,interfaceB,分别拥有两个实现类,aa\aaa与bb\bbb,上述结构的作用对应下图,浅蓝色小框表示被实例化的实例对象,尽管有重复的框,但表示的都是同一个对象
在这里插入图片描述

根据源码与上图,我们可以得出执行流程,当调用ExtensionLoader.getExtensionLoader(A.class).getExtension("aa");

  1. getExtensionLoader(A.class)先从EXTENSION_LOADERS中获取ExtensionLoader<A>
  2. 如果获取不到,则新建一个,并加入EXTENSION_LOADERS中
  3. 接着执行ExtensionLoader<A>的getExtension(“aa”),从cachedInstances中获取"aa"对应的实例
  4. 有则返回,没有则通过createExtension创建
  5. createExtension创建过程中,查询cachedClasses是否有内容,没有则从配置文件中加载内容,并完成类的加载
  6. 有则获取“aa”对应的类,并返回
  7. createExtension过程从cachedClasses获取到对应的类后,通过类名从EXTENSION_INSTANCES进行获取,如果能够获取到实例,则返回,不能则进行实例化,并将结果同步到EXTENSION_INSTANCES中

并发问题分析

在上述过程中,哪些步骤会有并发安全问题呢?最常见的就是先查询再插入这一过程,我们挨个进行分析

首先,上述四个结构中,四个都是Map,除了cachedClasses,其余三个都为ConcurrentMap
源码中常常是这么使用ConcurrentMap的

            concurrentMap.putIfAbsent(name, new Object(name));
            object = concurrentMap.get(name);
  • 这种方式相当于CAS,看谁占位成功,就用谁的结果,这样能够保证插入与获取的一致性,以及保证线程安全
  • 例如,假设同时有两个线程执行上述操作,线程A创建了实例a,线程B创建了实例b,两者同时进行插入,由于concurrentMap插入是上了同步锁的,因此只有一个线程能够成功,另一个会失败,假设实例a插入成功了,则线程B获取到的也是实例a
  • 如果使用HashMap,很可能在线程A刚插入实例a,线程B获取插入位置的状态还是无内容,从而也进行插入,线程A就会丢失修改了
  • 理解了上述道理,就能够理解
        ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
        if (extensionLoader == null) {
            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));
            extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
        }

接着进入getExtension这一步

    public T getExtension(String name) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("Extension name should not be null or empty.");
        }
        // firstly get from cache, if not hit, create one
        Holder<Object> holder = cachedInstances.get(name);
        if (holder == null) {
            cachedInstances.putIfAbsent(name, new Holder<>());
            holder = cachedInstances.get(name);
        }
        // create a singleton if no instance exists
        Object instance = holder.get();
        if (instance == null) {
            synchronized (holder) {
                instance = holder.get();
                if (instance == null) {
                    instance = createExtension(name);
                    holder.set(instance);
                }
            }
        }
        return (T) instance;
    }

这一步的实现有点奇怪,你可能会问Holder有啥用?

为什么不直接 cachedInstances.putIfAbsent(name,createExtension(name)),还要搞这么多步

先放着这个问题,我们先看看createExtension(name)怎么创建实例对象的

    private T createExtension(String name) {
        // load all extension classes of type T from file and get specific one by name
        Class<?> clazz = getExtensionClasses().get(name);
        if (clazz == null) {
            throw new RuntimeException("No such extension of name " + name);
        }
        // 根据类型从全局获取
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            try {
                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
                instance = (T) EXTENSION_INSTANCES.get(clazz);
            } catch (Exception e) {
                log.error(e.getMessage());
            }
        }
        return instance;
    }

首先调用getExtensionClasses().get(name)获取需要被实例化的类

    private Map<String, Class<?>> getExtensionClasses() {
        // get the loaded extension class from the cache
        Map<String, Class<?>> classes = cachedClasses.get();
        // double check
        // 这里加锁是避免重复类加载,必须保证每个类全局只加载一次
        if (classes == null) {
            synchronized (cachedClasses) {
                classes = cachedClasses.get();
                if (classes == null) {
                    classes = new HashMap<>();
                    // load all extensions from our extensions directory
                    loadDirectory(classes);
                    cachedClasses.set(classes);
                }
            }
        }
        return classes;
    }
  • loadDirectory会根据配置文件,将对应类进行类加载,如果在这里我们不加锁,那么并发操作发生时,多个线程同时判断classes为空,就会进行多次类初始化
  • 那么这里为什么不用concurrentMap.putIfAbsent(name, loadDirectory(classes));的方式?因为这样loadDirectory还是会被并发执行,无法保证类只加载一次
  • 所以上述方案为必须,因为用了synchronized,所以不需要使用ConcurrentHashMap

那么从getExtensionClasses().get(name)获取到对应类名后,先查看EXTENSION_INSTANCES中是否已经有对应实例了,因为在其他ExtensionLoader中可能也会实例化该类型,这种方式能够保证实例全局唯一,即单例

如果获取不到,则通过putIfAbsent方法进行实例化并插入,保证线程安全

最终createExtension能够返回全局唯一单例实例

我们继续思考为什么不直接 cachedInstances.putIfAbsent(name,createExtension(name)),还要搞这么多步?

  • 该方法会使得createExtension执行多次
  • 导致getExtensionClasses()也会执行多次
    • 但该方法使用synchronized+双重检查,因此loadDirectory无论如何也只会执行一次,故没问题
  • 导致EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance())会执行多次,即clazz.newInstance()会执行多次
    • 但由于类似CAS机制,最后也只会留下第一个占位成功的instance,故没问题
  • 所以我其实并不能明白为什么不直接cachedInstances.putIfAbsent(name,createExtension(name))

至于为什么要用Holder,是便于后面加锁synchronized (holder) ,细化锁的粒度,如果不new 一个Holder,那么加锁的时候就没有锁不到Holder这个粒度了

但目前分析下来感觉没必要

总结

从上面的分析,我们大概可以知道

  • 同一ExtensionLoader下,并发加载类时,保证只进行一次类加载,但如果同一个类出现在不同ExtensionLoader中,那么就会出现重复加载
  • 最终所有线程获取到的实例都是全局唯一单例,并存储在EXTENSION_INSTANCES中
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值