dubbo-SPI机制

2020-09-02  本文已影响0人  天命_6236

借用dubbo官网介绍的设计图,如下:

image

** 说明:图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。**

为了更好的从源码的角度去看设计,需要先理清楚SPI到底是什么东西~

一、基本介绍

SPI 全称为Service Provider Interface,是一种服务发现机制。

本质:将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。

好处:优势体现在可以动态的替换实现类,这样可以更好的为我们的程序提供扩展功能。

二、SPI示例

2.1 java SPI 示例

首先定义一个接口:

package com.test;

import com.alibaba.dubbo.common.extension.SPI;

// @SPI("apple")
public interface Fruit {

    void eat();

}

两个不同的实现类:

package com.test;

public class Apple implements Fruit {
    @Override
    public void eat() {
        System.out.println("Hello, I am apple");
    }
}
package com.test;

public class Banana implements Fruit {
    @Override
    public void eat() {
        System.out.println("Hello, I am Banana.");
    }
}

接下来 META-INF/services 文件夹下创建一个文件,名称为 Fruit 的全限定名 com.test.Fruit。文件内容为实现类的全限定的类名,如下:

image
com.test.Apple
com.test.Banana

测试如下:

package com.test;

import com.alibaba.dubbo.common.extension.ExtensionLoader;

import java.util.ServiceLoader;

public class JavaSPITest {

    public static void main(String[] args) {
        // java spi机制
        ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Fruit::eat);

        // dubbo spi机制
        ExtensionLoader<Fruit> extensionLoader =
                ExtensionLoader.getExtensionLoader(Fruit.class);
        Fruit apple = extensionLoader.getExtension("apple");
        apple.eat();
        Fruit banana = extensionLoader.getExtension("banana");
        banana.eat();
    }
}
Hello, I am apple
Hello, I am Banana.

Process finished with exit code 0

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在** META-INF/dubbo** 路径下(同样在该路径下根据接口的全限定名称创建文件),配置内容如下:

apple=com.test.Apple
banana=com.test.Banana

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

另外,还增加了IOC和AOP等特性,这部分在源码解析时会体现。

统一说明一下,Java SPI 和Dubbo SPI区别?

1.Java SPI虽然也实现了延迟加载,但是基本只能遍历全部,做不到按需进行指定实例化,导致了资源浪费。

2.Dubbo SPI实现了按需加载指定的实现类,另外还增加IOC和AOP等特性。

三、Dubbo SPI 源码分析

代码如下:

ExtensionLoader<Fruit> extensionLoader = ExtensionLoader.getExtensionLoader(Fruit.class);
Fruit apple = extensionLoader.getExtension("apple");

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

代码如下:

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

// ...

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
        if (type == null) {
            throw new IllegalArgumentException("Extension type == null");
        } else if (!type.isInterface()) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
        } else if (!withExtensionAnnotation(type)) {
            throw new IllegalArgumentException("Extension type(" + type + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
        } else {
            // loader从缓存中获取
            ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
            if (loader == null) {
                // 没有则新建,将loader放入map中
                EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
                loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
            }
            return loader;
        }
    }
public T getExtension(String name) {
        if (StringUtils.isEmpty(name)) {
            throw new IllegalArgumentException("Extension name == null");
        }
        if ("true".equals(name)) {
        // 获取默认的拓展实现类
            return getDefaultExtension();
        }
      // Holder,用于持有目标对象
        final Holder<Object> holder = getOrCreateHolder(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;
    }

重点来看看双重检查具体做了哪些事情,有哪些门道~上代码

package org.apache.dubbo.common.utils;

/**
* Helper Class for hold a value.
*/
public class Holder<T> {

   private volatile T value;

   public void set(T value) {
       this.value = value;
   }

   public T get() {
       return value;
   }

}

代码中使用了volatile关键字,有什么作用呢?具体请见解析java-volatile解析

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

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.通过 getExtensionClasses 获取所有的拓展类
2.通过反射创建拓展对象
3.向拓展对象中注入依赖
4.将拓展对象包裹在相应的 Wrapper 对象中
第一个步骤加载扩展类的关键,第三和第四个步骤是Dubbo IOC和AOP的具体实现。

3.1 获取所有的扩展类
在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可

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 方法的逻辑。

private Map<String, Class<?>> loadExtensionClasses() {
        // 提取以及缓存可能存在的默认扩展类名称
        cacheDefaultExtensionName();

        //加载策略
        Map<String, Class<?>> extensionClasses = new HashMap<>();

        for (LoadingStrategy strategy : strategies) {
            loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
        }

        return extensionClasses;
    }
/**
     * extract and cache default extension name if exists
     */
    private void cacheDefaultExtensionName() {
        final SPI defaultAnnotation = type.getAnnotation(SPI.class);
        if (defaultAnnotation == null) {
            return;
        }

        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));
            }
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。

加载策略代码如下:

private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();

// ...

private static LoadingStrategy[] loadLoadingStrategies() {
        return stream(load(LoadingStrategy.class).spliterator(), false)
                .sorted()
                .toArray(LoadingStrategy[]::new);
    }
    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }
LoadingStrategy.png

加载过程解析
ServiceLoader.load方法,利用的是jdk自带的spi机制,加载 LoadingStrategy类的实现类,
jdk的spi机制约定了,接口实现类的描述文件存放位置为:META-INF/services/,文件名称为接口的全限定名,即 org.apache.dubbo.common.extension.LoadingStrategy,文件内容如下:

org.apache.dubbo.common.extension.DubboInternalLoadingStrategy
org.apache.dubbo.common.extension.DubboLoadingStrategy
org.apache.dubbo.common.extension.ServicesLoadingStrategy

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件夹路径 + type 全限定名 
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        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("...");
    }
}
上一篇下一篇

猜你喜欢

热点阅读