来撸一撸Dubbo之SPI机制源码,SPI到底解决了什么问题?
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
定义接口和实现
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}添加配置文件
在工程里此文件夹META-INF/services创建一个名称为Car接口全路经的文件夹如com.test.spi.Car,文件内容为:
1com.test.spi.Lamber
2com.test.spi.Ferrari程序中使用
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是怎么实现的,原有的配方熟悉的味道,套路都是一样的。接口还是上面那一套,不再重新写一遍了,我们直接看不同点:
配置文件
Dubbo的配置文件放在工程里的META-INF/dubbo文件夹下,文件名称不变,依然是接口全路经,通过键值对的形式进行配置:
1lamber=com.test.spi.Lamber
2ferrari=com.test.spi.Ferrari程序中使用
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}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机制问题都会是小菜。