一、dubbo初体验之SPI

2022-12-02 19:35:45 浏览数 (2)

什么是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方法注入。

0 人点赞