Dubbo SPI

114 阅读10分钟

Dubbo的配置总线

Dubbo SPI

Dubbo为了更好地达到OCP原则(对扩展开放,对修改封闭),采用了“微内核+插件”的架构。

微内核架构被称为插件化架构,这是一种面向功能进行拆分的可扩展性架构。内核功能稳定,只负责管理插件的生命周期,不会因为系统功能的扩展而不断进行修改,功能上的扩展全部封装到插件中,插件模块是独立存在的模块,包含特定的功能,能扩展内核系统功能。

微内核架构中,通常采用Factory、IOC等方式管理插件生命周期,Dubbo最终采用SPI机制来加载插件。参考的是JDK原生的SPI机制。

JDK SPI

使用JAVA访问数据库时会使用到java.sql.Driver接口,不同数据库产品的底层的协议不同,提供的java.sql.Driver实现也不同。开发人员不清楚用户最终会使用到哪个数据库,这种情况可以使用JAVA SPI的机制。

机制

  1. 首先创建一个Log接口,来模拟日志打印的功能。

    ```
    public interface Log {
        void log(String info);
    }
    ```
    
  2. 提供两个实现——Logback和Log4j。

  3. 在项目的 resources/META-INF/services 目录下添加一个名为 com.xxx.Log 的文件,这是 JDK SPI 需要读取的配置文件。具体内容为:

    1. com.xxx.impl.Log4j 
      com.xxx.impl.Logback 
      
  4. 最后创建 main() 方法,其中会加载上述配置文件,创建全部 Log 接口实现的实例,并执行其 log() 方法。

    1. public class Main { 
          public static void main(String[] args) { 
              ServiceLoader<Log> serviceLoader =  
                      ServiceLoader.load(Log.class); 
              Iterator<Log> iterator = serviceLoader.iterator(); 
              while (iterator.hasNext()) { 
                  Log log = iterator.next(); 
                  log.log("JDK SPI");  
              } 
          } 
      }
      
      // 输出如下: 
      // Log4j:JDK SPI 
      // Logback:JDK SPI 
      

源码分析

JDK SPI 的入口方法是 ServiceLoader.load() 方法。在 ServiceLoader.load() 方法中,首先会尝试获取当前使用的 ClassLoader(获取当前线程绑定的 ClassLoader,查找失败后使用 SystemClassLoader),然后调用 reload() 方法。

在 reload() 方法中,首先会清理 providers 缓存(LinkedHashMap 类型的集合),该缓存用来记录 ServiceLoader 创建的实现对象,其中 Key 为实现类的完整类名,Value 为实现类的对象。之后创建 LazyIterator 迭代器,用于读取 SPI 配置文件并实例化实现类对象。

ServiceLoader.reload() 方法的具体实现,如下所示:

// 缓存,用来缓存 ServiceLoader创建的实现对象 
private LinkedHashMap<String,S> providers = new LinkedHashMap<>(); 
public void reload() { 
    providers.clear(); // 清空缓存 
    lookupIterator = new LazyIterator(service, loader); // 迭代器 
} 

示例中main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法:hasNext() 方法和 next() 方法。这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法,hasNext() 方法最终调用的是 hasNextService() 方法。

LazyIterator.hasNextService() 方法,该方法主要负责查找 META-INF/services 目录下的 SPI 配置文件,并进行遍历。实现如下:

private static final String PREFIX = "META-INF/services/"; 
Enumeration<URL> configs = null; 
Iterator<String> pending = null; 
String nextName = null; 
private boolean hasNextService() { 
    if (nextName != null) { 
        return true; 
    } 
    if (configs == null) { 
        // PREFIX前缀与服务接口的名称拼接起来,就是META-INF目录下定义的SPI配置文件(即示例中的META-INF/services/com.xxx.Log) 
        String fullName = PREFIX + service.getName(); 
        // 加载配置文件 if (loader == null) 
            configs = ClassLoader.getSystemResources(fullName); 
        else 
            configs = loader.getResources(fullName); 
    } 
    // 按行SPI遍历配置文件的内容 while ((pending == null) || !pending.hasNext()) {  
        if (!configs.hasMoreElements()) { 
            return false; 
        } 
        // 解析配置文件 
        pending = parse(service, configs.nextElement());  
    } 
    nextName = pending.next(); // 更新 nextName字段 return true; 
}  

在 hasNextService() 方法中完成 SPI 配置文件的解析之后,再来看 LazyIterator.nextService() 方法,该方法负责实例化 hasNextService() 方法读取到的实现类,其中会将实例化的对象放到 providers 集合中缓存起来

private S nextService() { 
    String cn = nextName; 
    nextName = null; 
    // 加载 nextName字段指定的类 
    Class<?> c = Class.forName(cn, false, loader); 
    if (!service.isAssignableFrom(c)) { // 检测类型 
        fail(service, "Provider " + cn  + " not a subtype"); 
    } 
    S p = service.cast(c.newInstance()); // 创建实现类的对象 
    providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存 return p; 
} 

以上就是在 main() 方法中使用的迭代器的底层实现。最后,我们再来看一下 main() 方法中使用ServiceLoader.iterator() 方法拿到的迭代器是如何实现的,这个迭代器是依赖 LazyIterator 实现的一个匿名内部类。

public Iterator<S> iterator() { 
    return new Iterator<S>() { 
        // knownProviders用来迭代providers缓存 
        Iterator<Map.Entry<String,S>> knownProviders 
            = providers.entrySet().iterator(); 
        public boolean hasNext() { 
            // 先走查询缓存,缓存查询失败,再通过LazyIterator加载 
            if (knownProviders.hasNext())  
                return true; 
            return lookupIterator.hasNext(); 
        } 
        public S next() { 
            // 先走查询缓存,缓存查询失败,再通过 LazyIterator加载 
            if (knownProviders.hasNext()) 
                return knownProviders.next().getValue(); 
            return lookupIterator.next(); 
        } 
        // 省略remove()方法 
    }; 
} 

Dubbo SPI

JDK SPI 在查找扩展实现类的过程中,需要遍历 SPI 配置文件中定义的所有实现类,该过程中会将这些实现类全部实例化。如果 SPI 配置文件中定义了多个实现类,而我们只需要使用其中一个实现类时,就会生成不必要的对象。例如,org.apache.dubbo.rpc.Protocol 接口有 InjvmProtocol、DubboProtocol、RmiProtocol、HttpProtocol、HessianProtocol、ThriftProtocol 等多个实现,如果使用 JDK SPI,就会加载全部实现类,导致资源的浪费。

而Dubbo SPI解决了上述资源而浪费的问题,还对SPI配置文件扩展和修改。

  1. 按照Dubbo按照SPI配置文件的用途,将其分成了三类目录。
  • META-INF/services/ 目录:该目录下的 SPI 配置文件用来兼容 JDK SPI 。
  • META-INF/dubbo/ 目录:该目录用于存放用户自定义 SPI 配置文件。
  • META-INF/dubbo/internal/ 目录:该目录用于存放 Dubbo 内部使用的 SPI 配置文件。
  1. 将SPI配置文件改成了KV格式。

例如:dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

key被称为扩展名,当我们在为一个接口查找具体实现类时,可以指定扩展名来选择相应的扩展实现。这样就可以通过指定的key只实例化一个扩展实现即可,无须实例化SPI配置文件中的其他扩展实现类。

使用KV格式的另一个好处就是更容易定位到问题。假设我们使用的一个扩展实现类所在的 jar 包没有引入到项目中,那么 Dubbo SPI 在抛出异常的时候,会携带该扩展名信息,而不是简单地提示扩展实现类无法加载。这些更加准确的异常信息降低了排查问题的难度,提高了排查问题的效率。

@SPI注解

某个接口被@SPI扩展修饰,就表示该接口是扩展接口。@SPI注解的value值指定了默认的扩展名称。

Dubbo SPI 的核心逻辑几乎都封装在 ExtensionLoader 之中,使用方式如下:

Protocol protocol = ExtensionLoader 
   .getExtensionLoader(Protocol.class).getExtension("dubbo");

ExtensionLoader 中三个核心的静态字段。

  • strategies(LoadingStrategy[]类型): LoadingStrategy 接口有三个实现(通过 JDK SPI 方式加载的)。优先级为DubboInternalLoadingStrategy > DubboLoadingStrategy > ServicesLoadingStrateg。
  • EXTENSION_LOADERS(ConcurrentMap<Class, ExtensionLoader>类型): Dubbo 中一个扩展接口对应一个 ExtensionLoader 实例,该集合缓存了全部 ExtensionLoader 实例,其中的 Key 为扩展接口,Value 为加载其扩展实现的 ExtensionLoader 实例。
  • EXTENSION_INSTANCES(ConcurrentMap<Class<?>, Object>类型) :该集合缓存了扩展实现类与其实例对象的映射关系。在前文示例中,Key 为 Class,Value 为 DubboProtocol 对象。

ExtensionLoader 的实例字段如下:

  • type(Class<?>类型) :当前 ExtensionLoader 实例负责加载扩展接口。
  • cachedDefaultName(String类型) :记录了 type 这个扩展接口上 @SPI 注解的 value 值,也就是默认扩展名。
  • cachedNames(ConcurrentMap<Class<?>, String>类型) :缓存了该 ExtensionLoader 加载的扩展实现类与扩展名之间的映射关系。
  • cachedClasses(Holder<Map<String, Class<?>>>类型) :缓存了该 ExtensionLoader 加载的扩展名与扩展实现类之间的映射关系。cachedNames 集合的反向关系缓存。
  • cachedInstances(ConcurrentMap<String, Holder <Object> >类型) :缓存了该 ExtensionLoader 加载的扩展名与扩展实现对象之间的映射关系。

ExtensionLoader.getExtensionLoader() 方法会根据扩展接口从 EXTENSION_LOADERS 缓存中查找相应的 ExtensionLoader 实例,实现如下:

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) { 
    ExtensionLoader<T> loader =
         (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); 
    if (loader == null) { 
        EXTENSION_LOADERS.putIfAbsent(type, 
               new ExtensionLoader<T>(type)); 
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type); 
    } 
    return loader; 
}

得到接口对应的 ExtensionLoader 对象之后会调用其 getExtension() 方法,根据传入的扩展名称从 cachedInstances 缓存中查找扩展实现的实例,最终将其实例化后返回:

public T getExtension(String name) { 
    // getOrCreateHolder()方法中封装了查找cachedInstances缓存的逻辑 
    Holder<Object> holder = getOrCreateHolder(name); 
    Object instance = holder.get(); 
    if (instance == null) { // double-check防止并发问题 synchronized (holder) { 
            instance = holder.get(); 
            if (instance == null) { 
                // 根据扩展名从SPI配置文件中查找对应的扩展实现类 
                instance = createExtension(name); 
                holder.set(instance); 
            } 
        } 
    } 
    return (T) instance; 
}

在 createExtension() 方法中完成了 SPI 配置文件的查找以及相应扩展实现类的实例化,同时还实现了自动装配以及自动 Wrapper 包装等功能。其核心流程是这样的:

  1. 获取 cachedClasses 缓存,根据扩展名从 cachedClasses 缓存中获取扩展实现类。如果 cachedClasses 未初始化,则会扫描前面介绍的三个 SPI 目录获取查找相应的 SPI 配置文件,然后加载其中的扩展实现类,最后将扩展名和扩展实现类的映射关系记录到 cachedClasses 缓存中。
  2. 根据扩展实现类从 EXTENSION_INSTANCES 缓存中查找相应的实例。如果查找失败,会通过反射创建扩展实现对象。
  3. 自动装配扩展实现对象中的属性(即调用其 setter)。
  4. 自动包装扩展实现对象。
  5. 如果扩展实现类实现了 Lifecycle 接口,在 initExtension() 方法中会调用 initialize() 方法进行初始化。
private T createExtension(String name) { 
    Class<?> clazz = getExtensionClasses().get(name); // --- 1 
    if (clazz == null) { 
        throw findException(name); 
    } 
    try { 
        T instance = (T) EXTENSION_INSTANCES.get(clazz); // --- 2 
        if (instance == null) { 
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); 
            instance = (T) EXTENSION_INSTANCES.get(clazz); 
        } 
        injectExtension(instance); // --- 3 
        Set<Class<?>> wrapperClasses = cachedWrapperClasses; // --- 4 
        if (CollectionUtils.isNotEmpty(wrapperClasses)) { 
            for (Class<?> wrapperClass : wrapperClasses) { 
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); 
            } 
        } 
        initExtension(instance); // ---5
        return instance; 
    } catch (Throwable t) { 
        throw new IllegalStateException("Extension instance (name: " + name + ", class: " + 
                type + ") couldn't be instantiated: " + t.getMessage(), t); 
    } 
}

@Adaptive注解与适配器

@Adaptive 注解用来实现 Dubbo 的适配器功能。

什么是适配器?

Dubbo 中的 ExtensionFactory 接口有三个实现类:SpiExtensionFactory、SpringExtensionFactory、AdaptiveExtensionFactory。ExtensionFactory 接口上有 @SPI 注解,AdaptiveExtensionFactory 实现类上有 @Adaptive 注解。

AdaptiveExtensionFactory 不实现任何具体的功能,而是用来适配 ExtensionFactory 的 SpiExtensionFactory 和 SpringExtensionFactory 这两种实现。AdaptiveExtensionFactory 会根据运行时的一些状态来选择具体调用 ExtensionFactory 的哪个实现。

@Adaptive 注解还可以加到接口方法之上,Dubbo 会动态生成适配器类。

ExtensionLoader.createExtension() 方法,其中在扫描 SPI 配置文件的时候,会调用 loadClass() 方法加载 SPI 配置文件中指定的类。loadClass() 方法中会识别加载扩展实现类上的 @Adaptive 注解,将该扩展实现的类型缓存到 cachedAdaptiveClass 这个实例字段上(volatile修饰):

private void loadClass(){ 
    if (clazz.isAnnotationPresent(Adaptive.class)) { 
        // 缓存到cachedAdaptiveClass字段 
        cacheAdaptiveClass(clazz, overridden);
    } else ... // 省略其他分支 
}

通过 ExtensionLoader.getAdaptiveExtension() 方法获取适配器实例,并将该实例缓存到 cachedAdaptiveInstance 字段(Holder类型)中,核心流程如下:

  • 首先,检查 cachedAdaptiveInstance 字段中是否已缓存了适配器实例,如果已缓存,则直接返回该实例即可。
  • 然后,调用 getExtensionClasses() 方法,其中就会触发前文介绍的 loadClass() 方法,完成cachedAdaptiveClass 字段的填充。
  • 如果存在 @Adaptive 注解修饰的扩展实现类,该类就是适配器类,通过 newInstance() 将其实例化即可。如果不存在 @Adaptive 注解修饰的扩展实现类,就需要通过 createAdaptiveExtensionClass() 方法扫描扩展接口中方法上的 @Adaptive 注解,动态生成适配器类,然后实例化。
  • 接下来,调用 injectExtension() 方法进行自动装配,就能得到一个完整的适配器实例。
  • 最后,将适配器实例缓存到 cachedAdaptiveInstance 字段,然后返回适配器实例。