Java|Apache Dubbo系列(增强SPI)

Dubbo良好的扩展性与两个方面是密不可分的,一是Dubbo整体架构中,在合适的场景中巧妙的使用了设计模式,二是使用Dubbo SPI机制,使Dubbo的接口与实现完全解耦。
在本次分享中,您可以了解如下知识点

  • Java SPI机制
  • Java SPI机制的缺点
  • Dubbo SPI配置规范
  • Dubbo SPI的分类与缓存
  • Dubbo SPI的特点
  • Dubbo SPI源码分析
  • Dubbo对IOC的支持
Java SPI机制 SPI,全称Service Provider Interface,起初是提供给厂商做定制化插件开发的。Java SPI使用策略模式,一个接口多种实现,我们只提供接口,具体实现并不在程序中直接确定,而是写在配置文件里。具体使用步骤如下:
1、定义一个接口和对应的方法。
package com.wb.service; public interface OrderService { void doOrder(); }

2、编写该接口的实现类,可以有多种实现。
package com.wb.service.impl; import com.wb.service.OrderService; public class OrderServiceImpl01 implements OrderService { @Override public void doOrder() { System.out.println("OrderServiceImpl01 run"); } public class OrderServiceImpl02 implements OrderService { @Override public void doOrder() { System.out.println("OrderServiceImpl02 run"); } }

3、在META-INF/services目录下新建一个文件,文件名是接口的全路径,如com.xxx.OrderService。
4、文件的具体内容是接口的实现类的全路径名称,如果接口有多个实现类,则换行编写。
com.wb.service.impl.OrderServiceImpl01 com.wb.service.impl.OrderServiceImpl02

5、在业务代码中使用java.util.ServiceLoader加载具体的实现类。
import com.wb.service.OrderService; import java.util.ServiceLoader; public class Test { public static void main(String[] args){ ServiceLoader loader = ServiceLoader.load(OrderService.class); for (OrderService orderService : loader) { orderService.doOrder(); } } } 输出结果: OrderServiceImpl01 run OrderServiceImpl02 run

Java SPI机制的缺点
Dubbo为什么不使用Java提供的SPI机制,而使用自己的SP?相对于Java SPI,Dubbo SPI做了一定的优化和改进。
1、Java SPI会一次性加载所有的扩展类,如果扩展类没有被使用到也会加载,很耗时。
2、如果扩展类加载失败,则连扩展的名称都获取不到,排查错误困难。
3、增加了对IOC和AOP的支持,一个扩展类可以通过setter方法注入到其他扩展类中。

Dubbo SPI机制 Dubbo SPI配置规范
Dubbo SPI和Java SPI类似,需要在META-INF/dubbo目录下放置相应的SPI配置文件,文件名是接口的全路径名,文件内容为key=扩展点实现类的全路径名,如果有多个实现类,则用换行符分割。其中,key是Dubbo SPI注解中传入的参数。另外,Dubbo SPI兼容了Java SPI,Dubbo在启动时会扫描META-INF/services/,META-INF/dubbo/、META-INF/dubbo/internal/三个路径下的SPI配置。详情可查看org.apache.dubbo.remoting.Transporter接口的实现类org.apache.dubbo.remoting.transport.netty4.NettyTransporter的配置,此处就不一一展开。
Dubbo SPI的分类与缓存
Dubbo SPI根据缓存类型可以分为以下两类:
  • Class缓存
    Dubbo SPI获取扩展类配置时,先从缓存中获取,如果缓存中没有,则加载配置文件,根据配置把Class缓存起来,但不会全部初始化。
  • 实例缓存
    基于性能考虑,Dubbo SPI不仅会缓存Class,也会缓存Class的实例对象。每次获取时先从缓存中获取,如果缓存中没有,则重新加载并缓存。这也是Dubbo SPI比Java SPI性能高的原因之一,按需加载。
也可以根据扩展类的种类分为以下类型:
  • 【Java|Apache Dubbo系列(增强SPI)】普通扩展类
    最基础的扩展类,在SPI中配置的扩展类。
  • 包装扩展类(Wrapper类)
    如果该类的构造方法里有普通扩展类作为参数,Dubbo会判定为扩展点 Wrapper 类。
  • 自适应扩展类(Adaptive类)
    这样的扩展接口有多种实现,具体使用哪个类不写死在配置中,而是通过URL参数动态确定(此处不展开解释)。
Dubbo SPI的特点
  • 自动包装特性
    如果该类的构造方法里有普通扩展类作为参数,Dubbo会判定为扩展点 Wrapper 类。
  • 自动加载特性
    如果该扩展类的成员变量有其他扩展类作为属性,并且拥有setter方法,那么Dubbo也会注入对应的扩展点实例(类似于Spring的依赖注入)。
  • 自适应
    在Dubbo中使用@Adaptive注解,我们可以在URL中动态的传入参数来确定具体使用哪个实现类。
Dubbo SPI源码分析
与Java SPI代码类似,Dubbo官网中有这么一段示例
public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } }

所以我们阅读源码的切入点是org.apache.dubbo.common.extension.ExtensionLoader#getExtension方法。
public T getExtension(String name) { if (StringUtils.isEmpty(name)) { throw new IllegalArgumentException("Extension name == null"); } // 获取默认的拓展实现类 if ("true".equals(name)) { return getDefaultExtension(); } // Holder,顾名思义,用于持有目标对象。通过name从缓存中获取 final Holder holder = getOrCreateHolder(name); Object instance = holder.get(); if (instance == null) { synchronized (holder) { instance = holder.get(); // 如果缓存为空,则创建实例并设置到Holder中 if (instance == null) { instance = createExtension(name); holder.set(instance); } } } return (T) instance; }
上面代码的逻辑比较简单,首先检查缓存,缓存未命中则创建拓展对象。下面我们来看一下创建扩展对象的过程是怎样的。
org.apache.dubbo.common.extension.ExtensionLoader#createExtension
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); } // 通过反射获取setter方法,向实例中注入依赖,体现了Dubbo SPI的自动加载特性 injectExtension(instance); Set> wrapperClasses = cachedWrapperClasses; if (CollectionUtils.isNotEmpty(wrapperClasses)) { // 循环创建 Wrapper 实例,体现出Dubbo SPI的自动包装特性 for (Class wrapperClass : wrapperClasses) { // 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。 // 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量 instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); } } initExtension(instance); return instance; } catch (Throwable t) { throw new IllegalStateException("Extension instance (name: " + name + ", class: " + type + ") couldn't be instantiated: " + t.getMessage(), t); } }

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:
  • 通过 getExtensionClasses 获取所有的拓展类
  • 通过反射创建拓展对象
  • 向拓展对象中注入依赖
  • 将拓展对象包裹在相应的 Wrapper 对象中
以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。接下来,将会重点分析 getExtensionClasses 方法的逻辑,以及简单介绍 Dubbo IOC 的具体实现。
org.apache.dubbo.common.extension.ExtensionLoader#getExtensionClasses
private Map> getExtensionClasses() { // 从缓存中获取 Map> classes = cachedClasses.get(); // 缓存中没有 if (classes == null) { synchronized (cachedClasses) { classes = cachedClasses.get(); if (classes == null) { // 从SPI配置文件中加载 classes = loadExtensionClasses(); cachedClasses.set(classes); } } } return classes; }

这段代码逻辑也很简单,下面分析 loadExtensionClasses 方法的逻辑
org.apache.dubbo.common.extension.ExtensionLoader#loadExtensionClasses
private Map> loadExtensionClasses() { // 获取SPI注解 final SPI defaultAnnotation = type.getAnnotation(SPI.class); String value = https://www.it610.com/article/defaultAnnotation.value(); if ((value = value.trim()).length()> 0) { // 检测 SPI 注解内容是否合法,不合法则抛出异常 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]; } } Map> extensionClasses = new HashMap<>(); // 加载指定文件夹下的配置文件 for (LoadingStrategy strategy : strategies) { loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.excludedPackages()); loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.excludedPackages()); } return extensionClasses; }

loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map> extensionClasses, String dir, String type, boolean extensionLoaderClassLoaderFirst, String... excludedPackages) { // 拼接文件名 String fileName = dir + type; try { Enumeration urls = null; ClassLoader classLoader = findClassLoader(); // 根据文件名加载所有的同名文件 if(urls == null || !urls.hasMoreElements()) { 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, excludedPackages); } } } catch (Throwable t) { logger.error("..."); } }

loadDirectory 方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源。我们继续跟下去,看一下 loadResource 方法的实现
private void loadResource(Map> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL, String... excludedPackages) { try { try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) { 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; // 通过=分割key和value int i = line.indexOf('='); if (i > 0) { name = line.substring(0, i).trim(); line = line.substring(i + 1).trim(); } if (line.length() > 0 && !isExcluded(line, excludedPackages)) { // 加载类,并通过 loadClass 方法对类进行缓存 loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); } } catch (Throwable t) { IllegalStateException e = new IllegalStateException("..."); exceptions.put(line, e); } } } } } catch (Throwable t) { logger.error("..."); } }

loadResource方法用于读取配置文件,最后通过loadClass方法对结果进行缓存,loadClass方法代码如下:
private void loadClass(Map> 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); } else if (isWrapperClass(clazz)) { // 检查类是否是包装类 cacheWrapperClass(clazz); } else { // 走到这一分支,是普通扩展剋 clazz.getConstructor(); if (StringUtils.isEmpty(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 注解,则使用 names 数组的第一个元素作为键, // 存储 name 到 Activate 注解对象的映射关系 cacheActivateClass(clazz, names[0]); for (String n : names) { // 存储 Class 到名称的映射关系 cacheName(clazz, n); // 存储名称到 Class 的映射关系 saveInExtensionClass(extensionClasses, clazz, n); } } } }

Dubbo对IOC的支持
Dubbo IOC 是通过 setter 方法注入依赖。Dubbo 首先会通过反射获取到实例的所有方法,然后再遍历方法列表,检测方法名是否具有 setter 方法特征。若有,则通过 ObjectFactory 获取依赖对象,最后通过反射调用 setter 方法将依赖设置到目标对象中
org.apache.dubbo.common.extension.ExtensionLoader#injectExtension
private T injectExtension(T instance) { if (objectFactory == null) { return instance; } try { // 获取所有setter方法 for (Method method : instance.getClass().getMethods()) { if (!isSetter(method)) { continue; } if (method.getAnnotation(DisableInject.class) != null) { continue; } // 获取 setter 方法参数类型 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) { // 通过反射调用 setter 方法设置依赖 method.invoke(instance, object); } } catch (Exception e) { logger.error("..."); } } } catch (Exception e) { logger.error(e.getMessage(), e); } return instance; }

好了,这是我读Dubbo官网所理解的知识点,今天的分享就到这里,下期再见。

扫描二维码,加作者微信,更多精彩
Java|Apache Dubbo系列(增强SPI)
文章图片

    推荐阅读