来撸一撸Dubbo之SPI机制源码,SPI到底解决了什么问题?

Posted 奇客时间

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了来撸一撸Dubbo之SPI机制源码,SPI到底解决了什么问题?相关的知识,希望对你有一定的参考价值。

纠结了很久,今年还是来整理个dubbo的系列知识吧!大概会总结下面的这些东西:

  1. Dubbo核心设计SPI机制源码剖析 (第一篇,老规矩不能太难)

  2. Dubbo配置源码剖析及外部化配置

  3. Dubbo服务本地和远程暴露

  4. Dubbo服务本地和远程引用

  5. Dubbo服务调用过程及负载均衡,路由剖析

  6. Dubbo线程池剖析

1.前言

SPI机制是Dubbo核心架构之一,也是面试中经常被问到的问题,所以想要学好Dubbo,想在面试中脱引而出,SPI是一道坎。今天彻底给大家讲清楚什么是SPI机制,以及Dubbo对该机制的使用,话不多说我们书归正传。

2.简介

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。

3.Java的SPI

  1. 定义接口和实现

     1public interface Car {
    2   void run();
    3}
    4public class Lamber implements Car {
    5
    6   @Override
    7   public void run() {
    8       System.out.println("Lamber, 400KM/h");
    9   }
    10}
    11public class Ferrari implements Car {
    12
    13   @Override
    14   public void run() {
    15       System.out.println("Ferrari, 500KM/h");
    16   }
    17}
  2. 添加配置文件

    在工程里此文件夹META-INF/services创建一个名称为Car接口全路经的文件夹如com.test.spi.Car,文件内容为:

    1com.test.spi.Lamber
    2com.test.spi.Ferrari
  3. 程序中使用

1public class JavaSPITest {
2    public static void main() throws Exception {
3        ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
4        serviceLoader.forEach(Robot::sayHello);
5    }
6}

4.Dubbo的SPI

通过上面的例子我们知道了什么是java的SPI,接下来看下Dubbo的SPI是怎么实现的,原有的配方熟悉的味道,套路都是一样的。接口还是上面那一套,不再重新写一遍了,我们直接看不同点:

  1. 配置文件

    Dubbo的配置文件放在工程里的META-INF/dubbo文件夹下,文件名称不变,依然是接口全路经,通过键值对的形式进行配置:

    1lamber=com.test.spi.Lamber
    2ferrari=com.test.spi.Ferrari
  2. 程序中使用

    1public class DubboSPITest {
    2   public static void main() throws Exception {
    3       ExtensionLoader<Car> extensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
    4       Car lamber = extensionLoader.getExtension("lamber");
    5       lamber.run();
    6       Car ferrari = extensionLoader.getExtension("ferrari");
    7       ferrari.run();
    8   }
    9}
  3. Dubbo SPI源码

    了解了Dubbo的SPI原理之后,我们就分析下Dubbo的源码里是怎么使用该机制的,通过上面的实例代码可以很清晰地看出第一步就是通过ExtensionLoader的静态方法getExtensionLoader(Car.class)来获取自适应拓展加载器。我们看下它的源码:

1public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
2     ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
3        if (loader == null) {
4          //如果本地缓存中没有找到,直接new出一个实例并放入本地缓存
5            EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
6            loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
7        }
8        return loader;
9    }

第二步就是通过名称从ExtensionLoader获取对应实现类:

 1public T getExtension(String name) {
2        if (StringUtils.isEmpty(name)) {
3            throw new IllegalArgumentException("Extension name == null");
4        }
5        if ("true".equals(name)) {
6          //获取默认的拓展类
7            return getDefaultExtension();
8        }
9              //对要加载的类进行包装
10        final Holder<Object> holder = getOrCreateHolder(name);
11        Object instance = holder.get();
12              //java并发知识应用,双重检查,比单纯使用synchronized速度要快
13        if (instance == null) {
14            synchronized (holder) {
15                instance = holder.get();
16                if (instance == null) {
17                  //创建拓展实例
18                    instance = createExtension(name);
19                    holder.set(instance);
20                }
21            }
22        }
23        return (T) instance;
24    }

接下来就看下createExtension(name)方法是怎么实现的:

 1private T createExtension(String name) {
2              //又是从本地缓存中先获取,所有加载过的拓展都会进入本地缓存,通过配置文件中的键值对获取到对应的接口实现类,如果找不到,说明配置有问题,直接抛出异常
3        Class<?> clazz = getExtensionClasses().get(name);
4        if (clazz == null) {
5            throw findException(name);
6        }
7        try {
8            T instance = (T) EXTENSION_INSTANCES.get(clazz);
9            if (instance == null) {
10              //反射创建实例并放入缓存,下次使用时直接获取本地缓存
11                EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
12                instance = (T) EXTENSION_INSTANCES.get(clazz);
13            }
14          //这个是实例的依赖注入,与Spring类似
15            injectExtension(instance);
16          //配置文件中有可能配置了很多包装类,在这里对上面创建的实例进行包装,成为一个链式结构,类似Spring的AOP的调用链,通过此循环就可以看出。
17            Set<Class<?>> wrapperClasses = cachedWrapperClasses;
18            if (CollectionUtils.isNotEmpty(wrapperClasses)) {
19                for (Class<?> wrapperClass : wrapperClasses) {
20                    instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
21                }
22            }
23            initExtension(instance);
24            return instance;
25        } catch (Throwable t) {
26            throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
27                    type + ") couldn't be instantiated: " + t.getMessage(), t);
28        }
29    }

接下来就是上一步中获取所有的拓展类的源码:

 1 private Map<String, Class<?>> getExtensionClasses() {
2               //本地缓存
3        Map<String, Class<?>> classes = cachedClasses.get();
4        if (classes == null) {
5            synchronized (cachedClasses) {
6                classes = cachedClasses.get();
7                if (classes == null) {
8                  //加载拓展类
9                    classes = loadExtensionClasses();
10                    cachedClasses.set(classes);
11                }
12            }
13        }
14        return classes;
15    }
16
17
18private Map<String, Class<?>> loadExtensionClasses() {
19          // 获取SPI注解并解析
20        cacheDefaultExtensionName();
21
22        Map<String, Class<?>> extensionClasses = new HashMap<>();
23                //通过java的SPI机制获取dubbo的配置文件夹
24        for (LoadingStrategy strategy : strategies) {
25            loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
26            loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache""com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
27        }
28
29        return extensionClasses;
30    }
31
32
33
34private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
35                               boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages)
 
{
36          //文件全路经
37        String fileName = dir + type;
38        try {
39            Enumeration<java.net.URL> urls = null;
40            ClassLoader classLoader = findClassLoader();
41
42            // try to load from ExtensionLoader's ClassLoader first
43            if (extensionLoaderClassLoaderFirst) {
44                ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
45                if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
46                    urls = extensionLoaderClassLoader.getResources(fileName);
47                }
48            }
49
50            if (urls == null || !urls.hasMoreElements()) {
51                if (classLoader != null) {
52                    urls = classLoader.getResources(fileName);
53                } else {
54                    urls = ClassLoader.getSystemResources(fileName);
55                }
56            }
57                        //循环遍历所有加载到的资源
58            if (urls != null) {
59                while (urls.hasMoreElements()) {
60                    java.net.URL resourceURL = urls.nextElement();
61                    loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
62                }
63            }
64        } catch (Throwable t) {
65            logger.error("Exception occurred when loading extension class (interface: " +
66                    type + ", description file: " + fileName + ").", t);
67        }
68    }

上面这么长的步骤就是为了找到要加载的资源,接下来就是对找到的资源,也就是我们的配置文件进行解析:

 1private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
2                              java.net.URL resourceURL, boolean overridden, String... excludedPackages)
 
{
3        try {
4            try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
5              //逐行解析配置文件
6                String line;
7                while ((line = reader.readLine()) != null) {
8                  //#在配置文件里为注释,要截取处理
9                    final int ci = line.indexOf('#');
10                    if (ci >= 0) {
11                        line = line.substring(0, ci);
12                    }
13                    line = line.trim();
14                    if (line.length() > 0) {
15                        try {
16                            String name = null;
17                          //等号前面为键值对的key,后面是value
18                            int i = line.indexOf('=');
19                            if (i > 0) {
20                                name = line.substring(0, i).trim();
21                                line = line.substring(i + 1).trim();
22                            }
23                            if (line.length() > 0 && !isExcluded(line, excludedPackages)) {
24                              //加载实现类并缓存
25                                loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name, overridden);
26                            }
27                        } catch (Throwable t) {
28                            IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);
29                            exceptions.put(line, e);
30                        }
31                    }
32                }
33            }
34        } catch (Throwable t) {
35            logger.error("Exception occurred when loading extension class (interface: " +
36                    type + ", class file: " + resourceURL + ") in " + resourceURL, t);
37        }
38    }

最后一步,真正去加载并实例化配置的拓展类:

 1 private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
2                           boolean overridden)
 throws NoSuchMethodException 
{
3        if (!type.isAssignableFrom(clazz)) {
4            throw new IllegalStateException("Error occurred when loading extension class (interface: " +
5                    type + ", class line: " + clazz.getName() + "), class "
6                    + clazz.getName() + " is not subtype of interface.");
7        }
8   //检测是不是自适应拓展类,自适应拓展类是一类特殊的拓展类
9        if (clazz.isAnnotationPresent(Adaptive.class)) {
10            cacheAdaptiveClass(clazz, overridden);
11        } else if (isWrapperClass(clazz)) {//是不是包装类,上面我们提到过,循环加载包装类,就是在这里获取到的包装类
12            cacheWrapperClass(clazz);
13        } else {
14          //这个是要加载的类的本尊,调用构造起函数进行实例化
15            clazz.getConstructor();
16          //如果没起名字,则使用默认名称
17            if (StringUtils.isEmpty(name)) {
18                name = findAnnotationName(clazz);
19                if (name.length() == 0) {
20                    throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
21                }
22            }
23
24            String[] names = NAME_SEPARATOR.split(name);
25            if (ArrayUtils.isNotEmpty(names)) {
26                cacheActivateClass(clazz, names[0]);
27                for (String n : names) {
28                  //放入缓存
29                    cacheName(clazz, n);
30                    saveInExtensionClass(extensionClasses, clazz, n, overridden);
31                }
32            }
33        }
34    }

4.总结

以上就是我们对dubbo的拓展机制的分析,相信如果你能认真看完这几个重要的类和方法,那么Dubbo使用和面试过程中的SPI机制问题都会是小菜。


以上是关于来撸一撸Dubbo之SPI机制源码,SPI到底解决了什么问题?的主要内容,如果未能解决你的问题,请参考以下文章

2020年分布式之Dubbo中SPI机制源码解析,学Java必看

dubbo源码阅读之SPI

2Dubbo 源码分析 —— 深入理解Dubbo 的增强SPI机制

Apache Dubbo 之 内核剖析

Apache Dubbo 之 内核剖析

Apache Dubbo 之 内核剖析