手写dubbo框架7-SPI(dubbo和jdk的区别)

2020-10-21 10:22:42 浏览数 (1)

博客中代码地址:https://github.com/farliu/farpc.git

这章继续了解SPI,上一章我们列举了dubbo选择SPI的背景和SPI的简单使用。不过,dubbo并未使用 Java 原生的SPI机制,而是对其进行了增强,使其能够更好的满足需求。我列举两点dubbo增强的优势。本章也对其进行展开。

  1. 按需加载接口实现类
  2. 增加了IOC和AOP等特性,向拓展对象中注入依赖
dubbo SPI示例

dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下。以下例子,取自dubbo的单测(dubbo-common模块)。

org.apache.dubbo.common.extension.ext1.SimpleExt

代码语言:javascript复制
# Comment 1
impl1=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl1#Hello World
impl2=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl2  # Comment 2
impl3=org.apache.dubbo.common.extension.ext1.impl.SimpleExtImpl3 # with head space

ExtensionLoaderTest

代码语言:javascript复制
@Test
public void test_getExtension() throws Exception {
    assertTrue(ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension("impl1") instanceof SimpleExtImpl1);
    assertTrue(ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension("impl2") instanceof SimpleExtImpl2);
}

结果单测肯定是通过的。也就是说我们通过impl1和impl2分别获取到了各自的实现类的对象。

总揽全局原理

我们粗略归纳一下,以上单测的运行逻辑,主要包含两个方法getExtensionLoader()和getExtension(),前者用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例,并创建一个objectFactory用于实现IOC注入。

代码语言:javascript复制
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 an interface!");
    }
    if (!withExtensionAnnotation(type)) {
        throw new IllegalArgumentException("Extension type ("   type  
                ") is not an extension, because it is NOT annotated with @"   SPI.class.getSimpleName()   "!");
    }

    //从缓存中获取与拓展类对应的 ExtensionLoader
    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;
}

/**
 * 创建ExtensionLoader对象
 **/
private final ExtensionFactory objectFactory;
private ExtensionLoader(Class<?> type) {
    this.type = type;
    //用于后续注入其他的扩展对象
    objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

getExtension()代码的逻辑比较简单,首先检查缓存,缓存未命中则调用createExtension()创建拓展对象。值得一提的是Holder,该类使用volatile修饰,使用Holder包装保证可见性。看源码更重要的时候学到这些细节,dubbo中对细节处理很到位,很多地方用到了双重检查和缓存等优化,这些平常到不能再平常的处理,让我对dubbo源码心生敬畏。

代码语言:javascript复制
public T getExtension(String name) {
    //一系列安全检查
    if (StringUtils.isEmpty(name)) {
        throw new IllegalArgumentException("Extension name == null");
    }
    if ("true".equals(name)) {
        // 获取默认的拓展实现类
        return getDefaultExtension();
    }

    //从缓存中获取,没有则new一个并保存
    Holder<Object> holder = getOrCreateHolder(name);
    Object instance = holder.get();
    // 双重检查
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 创建拓展实例
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

createExtension()是SPI一览全景的方法,这个方法包含了创建对象的所有精髓。

代码语言: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("...");
    }
}

createExtension()方法主要包含了如下的步骤:

  1. 根据传入的扩展名获取到对应实现类
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的Wrapper对象中

第一步是加载拓展类的关键,第二步是SPI的核心,第三和第四个步骤是dubbo IOC与AOP的具体实现。接下来主要细品这些内容。

细品:按需加载接口实现类

getExtensionClasses()该方法配置文件中加载所有的拓展类,返回Map<string, class>用来保存“配置项名称”到“配置类”的关系。代码逻辑简单,就是常规的使用DCL检查缓存,最后通过 loadExtensionClasses()加载拓展类。我们以loadExtensionClasses()为入口进行分析。

代码语言:javascript复制
private Map<String, Class<?>> loadExtensionClasses() {
    //从接口上的SPI注解里提取并缓存默认扩展名
    cacheDefaultExtensionName();

    // 加载指定文件夹下的配置文件,由于我的源码是2.7 ,所以有对alibaba的包名做兼容处理
    Map<String, Class<?>> extensionClasses = new HashMap<>();
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
    loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
    return extensionClasses;
}

loadExtensionClasses()一共做两件事,一是从接口上的SPI注解里提取并缓存默认扩展名,这段代码逻辑简单。二是调用loadDirectory()加载制定目录下所有的配置文件,我们重点关注第二点。

代码语言:javascript复制
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // 文件夹路径   type 全限定名。
    // 例如:META-INF/dubbo/internal/com.alibaba.dubbo.common.extension.ext1.SimpleExt
    String fileName = dir   type.getName();
    try {
        Enumeration<java.net.URL> urls;
        // 通过一切办法得到一个非空的classLoader
        ClassLoader classLoader = findClassLoader();
        // 加载同名的所有资源文件
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } 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("...");
    }
}

loadDirectory()主要是通过classLoader加载和fileName同名的所有资源文件,这里的资源也就是配置文件,然后调用loadResource()解析配置文件。其实findClassLoader()也建议大家好好端详一番,感受一下大佬的细致。

代码语言: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...");
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class...");
    }
}

loadResource()就是读取文件内容并解析,通过Class.forName()加载类,然后调用loadClass操作缓存。

代码语言:javascript复制
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("Error occurred when loading extension class (interface: "  
                type   ", class line: "   clazz.getName()   "), class "
                  clazz.getName()   " is not subtype of interface.");
    }
    // 检测类是否标注Adaptive注解,使用一个变量保存起来
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    } 
    // 检测class是否是Wapper类型,使用一个变量保存起来
    else if (isWrapperClass(clazz)) {
        cacheWrapperClass(clazz);
    } else {
        clazz.getConstructor();
        if (StringUtils.isEmpty(name)) {
            // name为空,则尝试从 Extension 注解中获取 name
            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 (ArrayUtils.isNotEmpty(names)) {
            // 如果存在Activate注解,存储name到Activate注解对象的映射关系
            cacheActivateClass(clazz, names[0]);
            for (String n : names) {
                // 保存Class到名称的关系
                cacheName(clazz, n);
                // 保存名称到Class的关系到extensionClasses,以此返回
                saveInExtensionClass(extensionClasses, clazz, name);
            }
        }
    }
}

至此,第一个特性按需加载接口实现类,所有实现都结束了,这个过程逻辑并不复杂,顺着这个思路或跟着断点走一遍基本上心里都有个七七八八。createExtension()的第二步通过反射获取对象,这里没有过多追述的。

细品:IOC 和 AOP 特性
SPI中的IOC

dubbo IOC是通过判断是否存在set方法,通过前文说的ObjectFactory对象获取注入对象。然后将注入对象通过反射调用set方法赋值到目标对象中。

代码语言:javascript复制
private T injectExtension(T instance) {
    try {
        if (objectFactory != null) {
            for (Method method : instance.getClass().getMethods()) {
                if (isSetter(method)) {
                    // 判断是否禁用注入,可用来防止覆盖已有的值
                    if (method.getAnnotation(DisableInject.class) != null) {
                        continue;
                    }
                    // 获取set方法的入参类型,就是需要注入的对象的class
                    Class<?> pt = method.getParameterTypes()[0];
                    if (ReflectUtils.isPrimitives(pt)) {
                        continue;
                    }
                    try {
                        String property = getSetterProperty(method);
                        // 从 ObjectFactory 中获取依赖对象
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
                            // 反射调用set方法
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
                        logger.error("Failed to inject via method "   method.getName()
                                  " of interface "   type.getName()   ": "   e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
        logger.error(e.getMessage(), e);
    }
    return instance;
}

讲起来,这就是IOC的所有的代码了,还是比较容易理解。这里可以继续深入的就是objectFactory.getExtension(),ExtensionFactory每个实现类的getExtension()代码都容易理解。只是多提一下,ExtensionFactory三个实现类的作用。SpiExtensionFactory用于创建自适应的拓展、SpringExtensionFactory用于从Spring的IOC容器中获取所需的拓展、AdaptiveExtensionFactory内部为一个List,遍历所有的getExtension()。

SPI中的AOP

dubbo对于SPI所增强的AOP,根本原理是在目标对象上包了一层Wrapper类,Wrapper也实现了目标接口,通过Wrapper的构造将目标对象保存至Wrapper对象中,而ExtensionLoader 返回扩展对象时,返回的Wrapper类的对象。我们将扩展对象的公共逻辑移至Wrapper类中,达到AOP的效果。Wrapper可以根据需要新增,切面的执行顺序按照配置文件中顺序决定。

了解了基础原理,我们先看一个简单的demo,该代码取自dubbo-common中的单元测试。

org.apache.dubbo.common.extension.ext6_wrap.WrappedExt

代码语言:javascript复制
impl1=org.apache.dubbo.common.extension.ext6_wrap.impl.Ext5Impl1
impl2=org.apache.dubbo.common.extension.ext6_wrap.impl.Ext5Impl2
wrapper1=org.apache.dubbo.common.extension.ext6_wrap.impl.Ext5Wrapper1
wrapper2=org.apache.dubbo.common.extension.ext6_wrap.impl.Ext5Wrapper2

ExtensionLoaderTest

代码语言:javascript复制
@Test
public void test_getExtension_WithWrapper() throws Exception {
    WrappedExt impl1 = ExtensionLoader.getExtensionLoader(WrappedExt.class).getExtension("impl1");
    assertThat(impl1, anyOf(instanceOf(Ext5Wrapper1.class), instanceOf(Ext5Wrapper2.class)));

    WrappedExt impl2 = ExtensionLoader.getExtensionLoader(WrappedExt.class).getExtension("impl2");
    assertThat(impl2, anyOf(instanceOf(Ext5Wrapper1.class), instanceOf(Ext5Wrapper2.class)));
}

我们打断点,先证实我上面所说的原理,观察impl1所返回的对象来自哪个类。

可以看到ExtensionLoader返回的扩展对象并非Ext5Impl1,而是Ext5Wrapper1,而Ext5Wrapper1中存在一个instance变量,该变量存的对象是Ext5Impl2,Ext5Impl2中的instance存的才是Ext5Impl1的对象。也就是说当我们调用WrappedExt接口中的方法时,会依次经过Ext5Wrapper1 > Ext5Wrapper2 > Ext5Impl1。

这也印证了之前说的执行顺序,执行顺序除了受配置文件中的顺序决定以外,还有一点值得注意。在上文分析的loadExtensionClasses()方法中有loadDirectory()一说,Wrapper的执行还跟加载文件夹有关。也就是:internal > META-INF/dubbo/ > META-INF/services/

基本基础知识了解了,再来看看怎么实现的,其实在上文中,都隐约有提到对于Wrapper的东西。这里再次将关键代码提取出来,应该更能理解。

createExtension() > getExtensionClasses() > loadExtensionClasses() > loadDirectory() > loadResource() > loadClass()

代码语言:javascript复制
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
    ...
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        cacheAdaptiveClass(clazz);
    } else if (isWrapperClass(clazz)) {
        if (cachedWrapperClasses == null) {
            cachedWrapperClasses = new ConcurrentHashSet<>();
        }
    // 将Wrapper类保存到Set<Class<?>>中
        cachedWrapperClasses.add(clazz);
    } else {
       ...
    }
}

private boolean isWrapperClass(Class<?> clazz) {
    try {
        // 根据是否存在参数为扩展对象的构造,来判断是否是Wrapper类
        clazz.getConstructor(type);
        return true;
    } catch (NoSuchMethodException e) {
        return false;
    }
}

createExtension()

代码语言:javascript复制
private T createExtension(String name) {
...
  Set<Class<?>> wrapperClasses = cachedWrapperClasses;
    // 判断该接口,存不存在Wrapper类
  if (CollectionUtils.isNotEmpty(wrapperClasses)) {
        // 如果有,那就一层一层的包起来,赋值给instance
      for (Class<?> wrapperClass : wrapperClasses) {
          instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
      }
  }
    return instance;
...
}

至此,dubbo SPI的特性全部解析清楚,剩下一个扩展点自适应机制,该机制逻辑晦涩难懂,这里讲诉篇幅过长,后续再做安排。本章有点啰嗦,后续可能不会这么详细,尽快的覆盖到整个dubbo,再做细致讲解。

0 人点赞