美文网首页
dubbo-SPI机制

dubbo-SPI机制

作者: 天命_6236 | 来源:发表于2020-09-02 09:21 被阅读0次

    借用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("...");
        }
    }
    

    相关文章

      网友评论

          本文标题:dubbo-SPI机制

          本文链接:https://www.haomeiwen.com/subject/bfrvsktx.html