写点什么

剖析 SPI 在 Spring 中的应用

  • 2022 年 6 月 21 日
  • 本文字数:19039 字

    阅读完需:约 62 分钟

vivo 互联网服务器团队 - Ma Jian

一、概述


SPI(Service Provider Interface),是 Java 内置的一种服务提供发现机制,可以用来提高框架的扩展性,主要用于框架的开发中,比如 Dubbo,不同框架中实现略有差异,但核心机制相同,而 Java 的 SPI 机制可以为接口寻找服务实现。SPI 机制将服务的具体实现转移到了程序外,为框架的扩展和解耦提供了极大的便利。

得益于 SPI 优秀的能力,为模块功能的动态扩展提供了很好的支撑。

本文会先简单介绍 Java 内置的 SPI 和 Dubbo 中的 SPI 应用,重点介绍分析 Spring 中的 SPI 机制,对比 Spring SPI 和 Java 内置的 SPI 以及与 Dubbo SPI 的异同。

二、Java SPI


Java 内置的 SPI 通过 java.util.ServiceLoader 类解析 classPath 和 jar 包的 META-INF/services/目录 下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用。

2.1 Java SPI

先通过代码来了解下 Java SPI 的实现

① 创建服务提供接口

package jdk.spi;// 接口public interface DataBaseSPI {    public void dataBaseOperation();}
复制代码

② 创建服务提供接口的实现类

  • MysqlDataBaseSPIImpl

实现类 1

package jdk.spi.impl; import jdk.spi.DataBaseSPI; public class MysqlDataBaseSPIImpl implements DataBaseSPI {     @Override    public void dataBaseOperation() {        System.out.println("Operate Mysql database!!!");    }}
复制代码


  • OracleDataBaseSPIImpl

实现类 2

package jdk.spi.impl; import jdk.spi.DataBaseSPI; public class OracleDataBaseSPIImpl implements DataBaseSPI {     @Override    public void dataBaseOperation() {        System.out.println("Operate Oracle database!!!");    }}
复制代码


③ 在项目 META-INF/services/目录下创建 jdk.spi.DataBaseSPI 文件


jdk.spi.DataBaseSPI

jdk.spi.impl.MysqlDataBaseSPIImpljdk.spi.impl.OracleDataBaseSPIImpl
复制代码


④ 运行代码:

JdkSpiTest#main()

package jdk.spi; import java.util.ServiceLoader; public class JdkSpiTest {     public static void main(String args[]){        // 加载jdk.spi.DataBaseSPI文件中DataBaseSPI的实现类(懒加载)        ServiceLoader<DataBaseSPI> dataBaseSpis = ServiceLoader.load(DataBaseSPI.class);        // ServiceLoader实现了Iterable,故此处可以使用for循环遍历加载到的实现类        for(DataBaseSPI spi : dataBaseSpis){            spi.dataBaseOperation();        }    }}
复制代码


⑤ 运行结果:

Operate Mysql database!!!Operate Oracle database!!!
复制代码

2.2 源码分析

上述实现即为使用 Java 内置 SPI 实现的简单示例,ServiceLoader 是 Java 内置的用于查找服务提供接口的工具类,通过调用 load()方法实现对服务提供接口的查找(严格意义上此步并未真正的开始查找,只做初始化),最后遍历来逐个访问服务提供接口的实现类。

上述访问服务实现类的方式很不方便,如:无法直接使用某个服务,需要通过遍历来访问服务提供接口的各个实现,到此很多同学会有疑问:

  • Java 内置的访问方式只能通过遍历实现吗?

  • 服务提供接口必须放到 META-INF/services/目录下?是否可以放到其他目录下?

在分析源码之前先给出答案:两个都是的;Java 内置的 SPI 机制只能通过遍历的方式访问服务提供接口的实现类,而且服务提供接口的配置文件也只能放在 META-INF/services/目录下。


ServiceLoader 部分源码

public final class ServiceLoader<S> implements Iterable<S>{    // 服务提供接口对应文件放置目录    private static final String PREFIX = "META-INF/services/";     // The class or interface representing the service being loaded    private final Class<S> service;     // 类加载器    private final ClassLoader loader;     // The access control context taken when the ServiceLoader is created    private final AccessControlContext acc;     // 按照初始化顺序缓存服务提供接口实例    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();     // 内部类,实现了Iterator接口    private LazyIterator lookupIterator;}
复制代码

从源码中可以发现:

  • ServiceLoader 类本身实现了 Iterable 接口并实现了其中的 iterator 方法,iterator 方法的实现中调用了 LazyIterator 这个内部类中的方法,解析完服务提供接口文件后最终结果放在了 Iterator 中返回,并不支持服务提供接口实现类的直接访问。

  • 所有服务提供接口的对应文件都是放置在 META-INF/services/目录下,final 类型决定了 PREFIX 目录不可变更。

所以 Java 内置的 SPI 机制思想是非常好的,但其内置实现上的不足也很明显。

三、Dubbo SPI

Dubbo SPI 沿用了 Java SPI 的设计思想,但在实现上有了很大的改进,不仅可以直接访问扩展类,而且在访问的灵活性和扩展的便捷性都做了很大的提升。

3.1 基本概念

① 扩展点

一个 Java 接口,等同于服务提供接口,需用 @SPI 注解修饰。

② 扩展

扩展点的实现类。

③ 扩展类加载器:ExtensionLoader

类似于 Java SPI 的 ServiceLoader,主要用来加载并实例化扩展类。一个扩展点对应一个扩展加载器。

④ Dubbo 扩展文件加载路径

Dubbo 框架支持从以下三个路径来加载扩展类:

  • META-INF/dubbo/internal

  • META-INF/dubbo

  • META-INF/services


Dubbo 框架针对三个不同路径下的扩展配置文件对应三个策略类:

  • DubboInternalLoadingStrategy

  • DubboLoadingStrategy

  • ServicesLoadingStrategy


三个路径下的扩展配置文件并没有特殊之处,一般情况下:

  • META-INF/dubbo 对开发者开放

  • META-INF/dubbo/internal 用来加载 Dubbo 内部的扩展点

  • META-INF/services 兼容 Java SPI

⑤ 扩展配置文件

和 Java SPI 不同,Dubbo 的扩展配置文件中扩展类都有一个名称,便于在应用中引用它们。

如:Dubbo SPI 扩展配置文件

#扩展实例名称=扩展点实现类adaptive=org.apache.dubbo.common.compiler.support.AdaptiveCompilerjdk=org.apache.dubbo.common.compiler.support.JdkCompilerjavassist=org.apache.dubbo.common.compiler.support.JavassistCompiler
复制代码

3.2 Dubbo SPI

先通过代码来演示下 Dubbo SPI 的实现。

① 创建扩展点(即服务提供接口)

扩展点

package dubbo.spi; import org.apache.dubbo.common.extension.SPI; @SPI  // 注解标记当前接口为扩展点public interface DataBaseSPI {    public void dataBaseOperation();}
复制代码

② 创建扩展点实现类

  • MysqlDataBaseSPIImpl

扩展类 1

package dubbo.spi.impl; import dubbo.spi.DataBaseSPI; public class MysqlDataBaseSPIImpl implements DataBaseSPI {     @Override    public void dataBaseOperation() {        System.out.println("Dubbo SPI Operate Mysql database!!!");    }}
复制代码
  • OracleDataBaseSPIImpl

扩展类 2

package dubbo.spi.impl; import dubbo.spi.DataBaseSPI; public class OracleDataBaseSPIImpl implements DataBaseSPI {     @Override    public void dataBaseOperation() {        System.out.println("Dubbo SPI Operate Oracle database!!!");    }}
复制代码

③在项目 META-INF/dubbo/目录下创建 dubbo.spi.DataBaseSPI 文件:

dubbo.spi.DataBaseSPI

#扩展实例名称=扩展点实现类mysql = dubbo.spi.impl.MysqlDataBaseSPIImploracle = dubbo.spi.impl.OracleDataBaseSPIImpl
复制代码

PS:文件内容中,等号左边为该扩展类对应的扩展实例名称,右边为扩展类(内容格式为一行一个扩展类,多个扩展类分为多行)


④ 运行代码:

DubboSpiTest#main()

package dubbo.spi; import org.apache.dubbo.common.extension.ExtensionLoader; public class DubboSpiTest {     public static void main(String args[]){        // 使用扩展类加载器加载指定扩展的实现        ExtensionLoader<DataBaseSPI> dataBaseSpis = ExtensionLoader.getExtensionLoader(DataBaseSPI.class);        // 根据指定的名称加载扩展实例(与dubbo.spi.DataBaseSPI中一致)        DataBaseSPI spi = dataBaseSpis.getExtension("mysql");        spi.dataBaseOperation();                 DataBaseSPI spi2 = dataBaseSpis.getExtension("oracle");        spi2.dataBaseOperation();    }}
复制代码


⑤ 运行结果:

Dubbo SPI Operate Mysql database!!!Dubbo SPI Operate Oracle database!!!
复制代码


从上面的代码实现直观来看,Dubbo SPI 在使用上和 Java SPI 比较类似,但也有差异。


相同:

  1. 扩展点即服务提供接口、扩展即服务提供接口实现类、扩展配置文件即 services 目录下的配置文件 三者相同。

  2. 都是先创建加载器然后访问具体的服务实现类,包括深层次的在初始化加载器时都未实时解析扩展配置文件来获取扩展点实现,而是在使用时才正式解析并获取扩展点实现(即懒加载)。


不同:

  1. 扩展点必须使用 @SPI 注解修饰(源码中解析会对此做校验)。

  2. Dubbo 中扩展配置文件每个扩展(服务提供接口实现类)都指定了一个名称。

  3. Dubbo SPI 在获取扩展类实例时直接通过扩展配置文件中指定的名称获取,而非 Java SPI 的循环遍历,在使用上更灵活。

3.3 源码分析

以上述的代码实现作为源码分析入口,了解下 Dubbo SPI 是如何实现的。


ExtensionLoader

① 通过 ExtensionLoader.getExtensionLoader(Classtype)创建对应扩展类型的扩展加载器。

ExtensionLoader#getExtensionLoader()

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {    if (type == null) {        throw new IllegalArgumentException("Extension type == null");    }    // 校验当前类型是否为接口    if (!type.isInterface()) {        throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");    }    // 接口上是否使用了@SPI注解    if (!withExtensionAnnotation(type)) {        throw new IllegalArgumentException("Extension type (" + type +                ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");    }    // 从内存中读取该扩展点的扩展类加载器    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);    // 内存中不存在则直接new一个扩展    if (loader == null) {        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);    }    return loader;}
复制代码

getExtensionLoader()方法中有三点比较重要的逻辑:

  1. 判断当前 type 类型是否为接口类型。

  2. 当前扩展点是否使用了 @SPI 注解修饰。

  3. EXTENSION_LOADERS 为 ConcurrentMap 类型的内存缓存,内存中存在该类型的扩展加载器则直接使用,不存在就 new 一个并放入内存缓存中。


再看下 new ExtensionLoader(type)源码

ExtensionLoader#ExtensionLoader()

// 私有构造器private ExtensionLoader(Class<?> type) {     this.type = type;     // 创建ExtensionFactory自适应扩展     objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); }
复制代码

重点:构造方法为私有类型,即外部无法直接使用构造方法创建 ExtensionLoader 实例。

每次初始化 ExtensionLoader 实例都会初始化 type 和 objectFactory ,type 为扩展点类型;objectFactory 为 ExtensionFactory 类型。


② 使用 getExtension()获取指定名称的扩展类实例 getExtension 为重载方法,分别为 getExtension(String name)和 getExtension(String name, boolean wrap),getExtension(String name)方法最终调用的还是 getExtension(String name, boolean wrap)方法。

ExtensionLoader#getExtension()

public T getExtension(String name) {     // 调用两个参数的getExtension方法,默认true表示需要对扩展实例做包装     return getExtension(name, true); }  public T getExtension(String name, boolean wrap) {    if (StringUtils.isEmpty(name)) {        throw new IllegalArgumentException("Extension name == null");    }    if ("true".equals(name)) {        return getDefaultExtension();    }    // 获取Holder实例,先从ConcurrentMap类型的内存缓存中取,没值会new一个并存放到内存缓存中    // Holder用来存放一个类型的值,这里用于存放扩展实例    final Holder<Object> holder = getOrCreateHolder(name);    // 从Holder读取该name对应的实例    Object instance = holder.get();    if (instance == null) {       // 同步控制       synchronized (holder) {          instance = holder.get();          // double check          if (instance == null) {             // 不存在扩展实例则解析扩展配置文件,实时创建             instance = createExtension(name, wrap);             holder.set(instance);          }        }     }     return (T) instance;}
复制代码

Holder 类:这里用来存放指定扩展实例


③ 使用 createExtension()创建扩展实例

ExtensionLoader#createExtension()

// 部分createExtension代码private T createExtension(String name, boolean wrap) {   // 先调用getExtensionClasses()解析扩展配置文件,并生成内存缓存,   // 然后根据扩展实例名称获取对应的扩展类   Class<?> clazz = getExtensionClasses().get(name);   if (clazz == null) {       throw findException(name);   }   try {       // 根据扩展类生成实例并对实例做包装(主要是进行依赖注入和初始化)       // 优先从内存中获取该class类型的实例       T instance = (T) EXTENSION_INSTANCES.get(clazz);       if (instance == null) {           // 内存中不存在则直接初始化然后放到内存中           EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());           instance = (T) EXTENSION_INSTANCES.get(clazz);       }       // 主要是注入instance中的依赖       injectExtension(instance);       ......}
复制代码

createExtension()方法:创建扩展实例,方法中 EXTENSION_INSTANCES 为 ConcurrentMap 类型的内存缓存,先从内存中取,内存中不存在重新创建;其中一个核心方法是 getExtensionClasses():


ExtensionLoader#getExtensionClasses()

private Map<String, Class<?>> getExtensionClasses() {   // 优先从内存缓存中读    Map<String, Class<?>> classes = cachedClasses.get();    if (classes == null) {        // 采用同步手段解析配置文件        synchronized (cachedClasses) {            // double check            classes = cachedClasses.get();            if (classes == null) {                // 正式开始解析配置文件                classes = loadExtensionClasses();                cachedClasses.set(classes);            }        }    }    return classes;}
复制代码

cachedClasses 为 Holder<map<string, class>>类型的内存缓存,getExtensionClasses 中会优先读内存缓存,内存中不存在则采用同步的方式解析配置文件,最终在 loadExtensionClasses 方法中解析配置文件,完成从扩展配置文件中读出扩展类:

ExtensionLoader#loadExtensionClasses()

// 在getExtensionClasses方法中是以同步的方式调用,是线程安全private Map<String, Class<?>> loadExtensionClasses() {   // 缓存默认扩展名称   cacheDefaultExtensionName();   Map<String, Class<?>> extensionClasses = new HashMap<>();   // strategies策略类集合,分别对应dubbo的三个配置文件目录   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;}
复制代码

源码中的 strategies 即 static volatile LoadingStrategy[] strategies 数组,通过 Java SPI 从 META-INF/services/目录下加载配置文件完成初始化,默认包含三个类:

  • DubboInternalLoadingStrategy

  • DubboLoadingStrategy

  • ServicesLoadingStrategy

分别对应 dubbo 的三个目录:

  • META-INF/dubbo/internal

  • META-INF/dubbo

  • META-INF/services


上述的源码分析只是对 Dubbo SPI 做了简要的介绍,Dubbo 中对 SPI 的应用很广泛,如:序列化组件、负载均衡等都应用了 SPI 技术,还有很多 SPI 功能未做分析,比如:自适应扩展、Activate 活性扩展等 等,感兴趣的同学可以更深入的研究。

四、Spring SPI

Spring SPI 沿用了 Java SPI 的设计思想,但在实现上和 Java SPI 及 Dubbo SPI 也存在差异,Spring 通过 spring.handlers 和 spring.factories 两种方式实现 SPI 机制,可以在不修改 Spring 源码的前提下,做到对 Spring 框架的扩展开发。

4.1 基本概念

  • DefaultNamespaceHandlerResolver

类似于 Java SPI 的 ServiceLoader,负责解析 spring.handlers 配置文件,生成 namespaceUri 和 NamespaceHandler 名称的映射,并实例化 NamespaceHandler。


  • spring.handlers

自定义标签配置文件;Spring 在 2.0 时便引入了 spring.handlers,通过配置 spring.handlers 文件实现自定义标签并使用自定义标签解析类进行解析实现动态扩,内容配置如:

http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandlerhttp\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandlerhttp\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler spring.handlers实现的SPI是以namespaceUri作为key,NamespaceHandler作为value,建立映射关系,在解析标签时通过namespaceUri获取相应的NamespaceHandler来解析
复制代码


  • SpringFactoriesLoader

类似于 Java SPI 的 ServiceLoader,负责解析 spring.factories,并将指定接口的所有实现类实例化后返回。


  • spring.factories

Spring 在 3.2 时引入 spring.factories,加强版的 SPI 配置文件,为 Spring 的 SPI 机制的实现提供支撑,内容配置如:

# PropertySource Loadersorg.springframework.boot.env.PropertySourceLoader=\org.springframework.boot.env.PropertiesPropertySourceLoader,\org.springframework.boot.env.YamlPropertySourceLoader # Run Listenersorg.springframework.boot.SpringApplicationRunListener=\org.springframework.boot.context.event.EventPublishingRunListener spring.factories实现的SPI是以接口的全限定名作为key,接口实现类作为value,多个实现类用逗号隔开,最终返回的结果是该接口所有实现类的实例集合
复制代码
  • 加载路径

Java SPI 从/META-INF/services 目录加载服务提供接口配置,而 Spring 默认从 META-INF/spring.handlers 和 META-INF/spring.factories 目录加载配置,其中 META-INF/spring.handlers 的路径可以通过创建实例时重新指定,而 META-INF/spring.factories 固定不可变。

4.2 spring.handlers

首先通过代码初步介绍下 spring.handlers 实现。

4.2.1 spring.handlers SPI

① 创建 NameSpaceHandler

MysqlDataBaseHandler

package spring.spi.handlers; import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.xml.NamespaceHandlerSupport;import org.springframework.beans.factory.xml.ParserContext;import org.w3c.dom.Element; // 继承抽象类public class MysqlDataBaseHandler extends NamespaceHandlerSupport {     @Override    public void init() {       }         @Override    public BeanDefinition parse(Element element, ParserContext parserContext) {        System.out.println("MysqlDataBaseHandler!!!");        return null;    }}
复制代码


OracleDataBaseHandler

package spring.spi.handlers; import org.springframework.beans.factory.config.BeanDefinition;import org.springframework.beans.factory.xml.NamespaceHandlerSupport;import org.springframework.beans.factory.xml.ParserContext;import org.w3c.dom.Element; public class OracleDataBaseHandler extends NamespaceHandlerSupport {     @Override    public void init() {    }     @Override    public BeanDefinition parse(Element element, ParserContext parserContext) {        System.out.println("OracleDataBaseHandler!!!");        return null;    }}
复制代码


② 在项目 META-INF/目录下创建 spring.handlers 文件:

文件内容:

spring.handlers

#一个namespaceUri对应一个handlerhttp\://www.mysql.org/schema/mysql=spring.spi.handlers.MysqlDataBaseHandlerhttp\://www.oracle.org/schema/oracle=spring.spi.handlers.OracleDataBaseHandler
复制代码


③ 运行代码:

SpringSpiTest#main()

package spring.spi; import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;import org.springframework.beans.factory.xml.NamespaceHandler; public class SpringSpiTest {     public static void main(String args[]){        // spring中提供的默认namespace URI解析器        DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver();        // 此处假设nameSpaceUri已从xml文件中解析出来,正常流程是在项目启动的时候会解析xml文件,获取到对应的自定义标签        // 然后根据自定义标签取得对应的nameSpaceUri        String mysqlNameSpaceUri = "http://www.mysql.org/schema/mysql";        NamespaceHandler  handler = resolver.resolve(mysqlNameSpaceUri);        // 验证自定义NamespaceHandler,这里参数传null,实际使用中传具体的Element        handler.parse(null, null);                 String oracleNameSpaceUri = "http://www.oracle.org/schema/oracle";        handler = resolver.resolve(oracleNameSpaceUri);        handler.parse(null, null);    }}
复制代码


④ 运行结果:

MysqlDataBaseHandler!!!OracleDataBaseHandler!!!
复制代码

上述代码通过解析 spring.handlers 实现对自定义标签的动态解析,以 NameSpaceURI 作为 key 获取具体的 NameSpaceHandler 实现类,这里有别于 Java SPI,其中:

DefaultNamespaceHandlerResolver 是 NamespaceHandlerResolver 接口的默认实现类,用于解析自定义标签。

  • DefaultNamespaceHandlerResolver.resolve(String namespaceUri)方法以 namespaceUri 作为参数,默认加载各 jar 包中的 META-INF/spring.handlers 配置文件,通过解析 spring.handlers 文件建立 NameSpaceURI 和 NameSpaceHandler 的映射。

  • 加载配置文件的默认路径是 META-INF/spring.handlers,但可以使用 DefaultNamespaceHandlerResolver(ClassLoader, String)构造方法修改,DefaultNamespaceHandlerResolver 有多个重载方法。

  • DefaultNamespaceHandlerResolver.resolve(String namespaceUri)方法主要被 BeanDefinitionParserDelegate 的 parseCustomElement()和 decorateIfRequired()方法中调用,所以 spring.handlers SPI 机制主要用在 bean 的扫描和解析过程中。

4.2.2 源码分析

下面从上述代码开始深入源码了解 spring handlers 方式实现的 SPI 是如何工作的。


  • DefaultNamespaceHandlerResolver

① DefaultNamespaceHandlerResolver.resolve()方法本身是根据 namespaceUri 获取对应的 namespaceHandler 对标签进行解析,核心源码:

DefaultNamespaceHandlerResolver#resolve()

public NamespaceHandler resolve(String namespaceUri) {    // 1、核心逻辑之一:获取namespaceUri和namespaceHandler映射关系    Map<String, Object> handlerMappings = getHandlerMappings();    // 根据namespaceUri参数取对应的namespaceHandler全限定类名or NamespaceHandler实例    Object handlerOrClassName = handlerMappings.get(namespaceUri);    if (handlerOrClassName == null) {        return null;    }    // 2、handlerOrClassName是已初始化过的实例则直接返回    else if (handlerOrClassName instanceof NamespaceHandler) {        return (NamespaceHandler) handlerOrClassName;    }else {        String className = (String) handlerOrClassName;        try {            ///3、使用反射根据namespaceHandler全限定类名加载实现类            Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);            if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {                throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +                        "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");            }            // 3.1、初始化namespaceHandler实例            NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);            // 3.2、 初始化,不同的namespaceHandler实现类初始化方法逻辑有差异            namespaceHandler.init();            // 4、将初始化好的实例放入内存缓存中,下次解析到相同namespaceUri标签时直接返回,避免再次初始化            handlerMappings.put(namespaceUri, namespaceHandler);            return namespaceHandler;        }catch (ClassNotFoundException ex) {            throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +                    namespaceUri + "] not found", ex);        }catch (LinkageError err) {            throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +                    namespaceUri + "]: problem with handler class file or dependent class", err);        }    }}
复制代码

第 1 步:源码中 getHandlerMappings()是比较核心的一个方法,通过懒加载的方式解析 spring.handlers 并返回 namespaceUri 和 NamespaceHandler 的映射关系。

第 2 步:根据 namespaceUri 返回对应的 NamespaceHandler 全限定名或者具体的实例(是名称还是实例取决于是否被初始化过,若是初始化过的实例会直接返回)

第 3 步:是 NamespaceHandler 实现类的全限定名,通过上述源码中的第 3 步,使用反射进行初始化。

第 4 步:将初始化后的实例放到 handlerMappings 内存缓存中,这也是第 2 步为什么可能是 NamespaceHandler 类型的原因。


看完 resolve 方法的源码,再看下 resolve 方法在 Spring 中调用场景,大致可以了解 spring.handlers 的使用场景:


可以看到 resolve()主要用在标签解析过程中,主要被在 BeanDefinitionParserDelegate 的 parseCustomElement 和 decorateIfRequired 方法中调用。


② resolve()源码中核心逻辑之一便是调用的 getHandlerMappings(),在 getHandlerMappings()中实现对各个 jar 包中的 META-INF/spring.handlers 文件的解析,如:

DefaultNamespaceHandlerResolver#getHandlerMappings()


private Map<String, Object> getHandlerMappings() { Map<String, Object> handlerMappings = this.handlerMappings; // 使用线程安全的解析逻辑,避免在并发场景下重复的解析,没必要重复解析 // 这里在同步代码块的内外对handlerMappings == null作两次判断很有必要,采用懒汉式初始化 if (handlerMappings == null) { synchronized (this) { handlerMappings = this.handlerMappings; // duble check if (handlerMappings == null) { if (logger.isDebugEnabled()) { logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]"); } try { // 加载handlerMappingsLocation目录文件,handlerMappingsLocation路径值可变,默认是META-INF/spring.handlers Properties mappings = PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader); if (logger.isDebugEnabled()) { logger.debug("Loaded NamespaceHandler mappings: " + mappings); } // 初始化内存缓存 handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size()); // 将加载到的属性合并到handlerMappings中 CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings); // 赋值内存缓存 this.handlerMappings = handlerMappings; }catch (IOException ex) { throw new IllegalStateException( "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex); } } } } return handlerMappings;}
复制代码

源码中 this.handlerMappings 是一个 Map 类型的内存缓存,存放解析到的 namespaceUri 以及 NameSpaceHandler 实例。

getHandlerMappings()方法体中的实现使用了线程安全方式,增加了同步逻辑。

通过阅读源码可以了解到 Spring 基于 spring.handlers 实现 SPI 逻辑相对比较简单,但应用却比较灵活,对自定义标签的支持很方便,在不修改 Spring 源码的前提下轻松实现接入,如 Dubbo 中定义的各种 Dubbo 标签便是很好的利用了 spring.handlers。


Spring 提供如此灵活的功能,那是如何应用的呢?下面简单了解下 parseCustomElement()。

  • BeanDefinitionParserDelegate.parseCustomElement()

resolve 作为工具类型的方法,被使用的地方比较多,这里仅简单介绍在 BeanDefinitionParserDelegate.parseCustomElement()中的应用。

BeanDefinitionParserDelegate#parseCustomElement()

public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {     // 获取标签的namespaceUri     String namespaceUri = getNamespaceURI(ele);     // 首先获得DefaultNamespaceHandlerResolver实例在再以namespaceUri作为参数调用resolve方法解析取得NamespaceHandler     NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);     if (handler == null) {         error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);         return null;     }     // 调用NamespaceHandler中的parse方法开始解析标签     return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); }
复制代码

parseCustomElement 作为解析标签的中间方法,再看下 parseCustomElement 的调用情况:


在 parseBeanDefinitions()中被调用,再看下 parseBeanDefinitions 的源码

DefaultBeanDefinitionDocumentReader#parseBeanDefinitions()

protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {    // spring内部定义的标签为默认标签,即非spring内部定义的标签都不是默认的namespace    if (delegate.isDefaultNamespace(root)) {        NodeList nl = root.getChildNodes();        for (int i = 0; i < nl.getLength(); i++) {            Node node = nl.item(i);            if (node instanceof Element) {                Element ele = (Element) node;                // root子标签也做此判断                if (delegate.isDefaultNamespace(ele)) {                    parseDefaultElement(ele, delegate);                }else{                    // 子标签非spring默认标签(即自定义标签)也走parseCustomElement来解析                    delegate.parseCustomElement(ele);                }            }        }    }else {        // 非spring的默认标签(即自定义的标签)走parseCustomElement来解析        delegate.parseCustomElement(root);    }}
复制代码

到此就很清晰了,调用前判断是否为 Spring 默认标签,不是默认标签调用 parseCustomElement 来解析,最后调用 resolve 方法。

4.2.3 小节

Spring 自 2.0 引入 spring.handlers 以后,为 Spring 的动态扩展提供更多的入口和手段,为自定义标签的实现提供了强力支撑。

很多文章在介绍 Spring SPI 时都重点介绍 spring.factories 实现,很少提及很早就引入的 spring.handlers,但通过个人的分析及与 Java SPI 的对比,spring.handlers 也是一种 SPI 的实现,只是基于 xml 实现。

相比于 Java SPI,基于 spring.handlers 实现的 SPI 更加的灵活,无需遍历,直接映射,更类似于 Dubbo SPI 的实现思想,每个类指定一个名称(只是 spring.handlers 中是以 namespaceUri 作为 key,Dubbo 配置中是指定的名称作为 key)。

4.3 spring.factories

同样先以测试代码来介绍 spring.factories 实现 SPI 的逻辑。

4.3.1 spring.factories SPI

① 创建 DataBaseSPI 接口

接口

package spring.spi.factories; public interface DataBaseSPI {    public void dataBaseOperation();}
复制代码


② 创建 DataBaseSPI 接口的实现类

MysqlDataBaseImpl

#实现类1package spring.spi.factories.impl; import spring.spi.factories.DataBaseSPI; public class MysqlDataBaseImpl implements DataBaseSPI {     @Override    public void dataBaseOperation() {        System.out.println("Mysql database test!!!!");    }}
复制代码


MysqlDataBaseImpl

#实现类2package spring.spi.factories.impl; import spring.spi.factories.DataBaseSPI; public class OracleDataBaseImpl implements DataBaseSPI {     @Override    public void dataBaseOperation() {        System.out.println("Oracle database test!!!!");    }}
复制代码

③ 在项目 META-INF/目录下创建 spring.factories 文件:


文件内容

spring.factories

#key是接口的全限定名,value是接口的实现类spring.spi.factories.DataBaseSPI = spring.spi.factories.impl.MysqlDataBaseImpl,spring.spi.factories.impl.OracleDataBaseImpl
复制代码

④ 运行代码

SpringSpiTest#main()

package spring.spi.factories; import java.util.List; import org.springframework.core.io.support.SpringFactoriesLoader; public class SpringSpiTest {     public static void main(String args[]){                 // 调用SpringFactoriesLoader.loadFactories方法加载DataBaseSPI接口所有实现类的实例        List<DataBaseSPI> spis= SpringFactoriesLoader.loadFactories(DataBaseSPI.class, Thread.currentThread().getContextClassLoader());                 // 遍历DataBaseSPI接口实现类实例        for(DataBaseSPI spi : spis){            spi.dataBaseOperation();        }    }}
复制代码

⑤ 运行结果

Mysql database test!!!!Oracle database test!!!!
复制代码


从上述的示例代码中可以看出 spring.facotries 方式实现的 SPI 和 Java SPI 很相似,都是先获取指定接口类型的实现类,然后遍历访问所有的实现。但也存在一定的差异:

(1)配置上:

Java SPI 是一个服务提供接口对应一个配置文件,配置文件中存放当前接口的所有实现类,多个服务提供接口对应多个配置文件,所有配置都在 services 目录下;

Spring factories SPI 是一个 spring.factories 配置文件存放多个接口及对应的实现类,以接口全限定名作为 key,实现类作为 value 来配置,多个实现类用逗号隔开,仅 spring.factories 一个配置文件。

(2)实现上

Java SPI 使用了懒加载模式,即在调用 ServiceLoader.load()时仅是返回了 ServiceLoader 实例,尚未解析接口对应的配置文件,在使用时即循环遍历时才正式解析返回服务提供接口的实现类实例;

Spring factories SPI 在调用 SpringFactoriesLoader.loadFactories()时便已解析 spring.facotries 文件返回接口实现类的实例(实现细节在源码分析中详解)。

4.3.2 源码分析

我们还是从测试代码开始,了解下 spring.factories 的 SPI 实现源码,细品 spring.factories 的实现方式。


  • SpringFactoriesLoader 测试代码入口直接调用 SpringFactoriesLoader.loadFactories()静态方法开始解析 spring.factories 文件,并返回方法参数中指定的接口类型,如测试代码里的 DataBaseSPI 接口的实现类实例。

SpringFactoriesLoader#loadFactories()

public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {    Assert.notNull(factoryClass, "'factoryClass' must not be null");    ClassLoader classLoaderToUse = classLoader;    // 1.确定类加载器    if (classLoaderToUse == null) {        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();    }    // 2.核心逻辑之一:解析各jar包中META-INF/spring.factories文件中factoryClass的实现类全限定名    List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);    if (logger.isTraceEnabled()) {        logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);    }    List<T> result = new ArrayList<T>(factoryNames.size());    // 3.遍历实现类的全限定名并进行实例化    for (String factoryName : factoryNames) {        result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));    }    // 排序    AnnotationAwareOrderComparator.sort(result);    // 4.返回实例化后的结果集    return result;}
复制代码

源码中 loadFactoryNames() 是另外一个比较核心的方法,解析 spring.factories 文件中指定接口的实现类的全限定名,实现逻辑见后续的源码。

经过源码中第 2 步解析得到实现类的全限定名后,在第 3 步通过 instantiateFactory()方法逐个实例化实现类。

再看 loadFactoryNames()源码是如何解析得到实现类全限定名的:

SpringFactoriesLoader#loadFactoryNames()

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {    // 1.接口全限定名    String factoryClassName = factoryClass.getName();    try {        // 2.加载META-INF/spring.factories文件路径(分布在各个不同jar包里,所以这里会是多个文件路径,枚举返回)        Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));        List<String> result = new ArrayList<String>();        // 3.遍历枚举集合,逐个解析spring.factories文件        while (urls.hasMoreElements()) {            URL url = urls.nextElement();            Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));            String propertyValue = properties.getProperty(factoryClassName);            // 4.spring.factories文件中一个接口的实现类有多个时会用逗号隔开,这里拆开获取实现类全限定名            for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {                result.add(factoryName.trim());            }        }        return result;    }catch (IOException ex) {        throw new IllegalArgumentException("Unable to load factories from location [" +                FACTORIES_RESOURCE_LOCATION + "]", ex);    }}
复制代码

源码中第 2 步获取所有 jar 包中 META-INF/spring.factories 文件路径,以枚举值返回。

源码中第 3 步开始遍历 spring.factories 文件路径,逐个加载解析,整合 factoryClass 类型的实现类名称。


获取到实现类的全限定名集合后,便根据实现类的名称逐个实例化,继续看下 instantiateFactory()方法的源码:

SpringFactoriesLoader#instantiateFactory()

private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {    try {        // 1.使用classLoader类加载器加载instanceClassName类        Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);        if (!factoryClass.isAssignableFrom(instanceClass)) {            throw new IllegalArgumentException(                    "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");        }        // 2.instanceClassName类中的构造方法        Constructor<?> constructor = instanceClass.getDeclaredConstructor();        ReflectionUtils.makeAccessible(constructor);        // 3.实例化        return (T) constructor.newInstance();    }    catch (Throwable ex) {        throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);    }}
复制代码

实例化方法是私有型(private)静态方法,这个有别于 loadFactories 和 loadFactoryNames。

实例化逻辑整体使用了反射实现,比较通用的实现方式。

通过对源码的分析,Spring factories 方式实现的 SPI 逻辑不是很复杂,整体上的实现容易理解。

Spring 在 3.2 便已引入 spring.factories,那 spring.factories 在 Spring 框架中又是如何使用的呢?先看下 loadFactories 方法的调用情况:

从调用情况看 Spring 自 3.2 引入 spring.factories SPI 后并没有真正的利用起来,使用的地方比较少,然而真正把 spring.factories 发扬光大的,是在 Spring Boot 中, 简单了解下 SpringBoot 中的调用。


  • getSpringFactoriesInstances()getSpringFactoriesInstances()并不是 Spring 框架中的方法,而是 SpringBoot 中 SpringApplication 类里定义的私有型(private)方法,很多地方都有调用,源码如下:

SpringApplication#getSpringFactoriesInstance()

// 单个参数getSpringFactoriesInstances方法private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {    // 默认调用多参的重载方法    return getSpringFactoriesInstances(type, new Class<?>[] {});}// 多个参数的getSpringFactoriesInstances方法private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,        Class<?>[] parameterTypes, Object... args) {    ClassLoader classLoader = getClassLoader();    // 调用SpringFactoriesLoader中的loadFactoryNames方法加载接口实现类的全限定名    Set<String> names = new LinkedHashSet<>(            SpringFactoriesLoader.loadFactoryNames(type, classLoader));    // 实例化    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,            classLoader, args, names);    AnnotationAwareOrderComparator.sort(instances);    return instances;}
复制代码

在 getSpringFactoriesInstances()中调用了 SpringFactoriesLoader.loadFactoryNames()来加载接口实现类的全限定名集合,然后进行初始化。

SpringBoot 中除了 getSpringFactoriesInstances()方法有调用,在其他逻辑中也广泛运用着 SpringFactoriesLoader 中的方法来实现动态扩展,这里就不在一一列举了,有兴趣的同学可以自己去发掘。

4.3.3 小节

Spring 框架在 3.2 引入 spring.factories 后并没有有效的利用起来,但给框架的使用者提供了又一个动态扩展的能力和入口,为开发人员提供了很大的自由发挥的空间,尤其是在 SpringBoot 中广泛运用就足以证明 spring.factories 的地位。spring.factories 引入在 提升 Spring 框架能力的同时也暴露出其中的不足:

首先,spring.factories 的实现类似 Java SPI,在加载到服务提供接口的实现类后需要循环遍历才能访问,不是很方便。

其次,Spring 在 5.0.x 版本以前 SpringFactoriesLoader 类定义为抽象类,但在 5.1.0 版本之后 Sping 官方将 SpringFactoriesLoader 改为 final 类,类型变化对前后版本的兼容不友好。

五、应用实践

介绍完 Spring 中 SPI 机制相关的核心源码,再来看看项目中自己开发的轻量版的分库分表 SDK 是如何利用 Spring 的 SPI 机制实现分库分表策略动态扩展的。

基于项目的特殊性并没有使用目前行业中成熟的分库分表组件,而是基于 Mybatis 的插件原理自己开发的一套轻量版分库分表组件。为满足不同场景分库分表要求,将其中分库分表的相关逻辑以策略模式进行抽取分离,每种分库分表的实现对应一条策略,支持使用方对分库分表策略的动态扩展,而这里的动态扩展就利用了 spring.factories。

首先给出轻量版分库分表组件流程图,然后我们针对流程图中使用到 Spring SPI 的地方进行详细分析。



说明:

  1. 上述流程图中项目启动过程中生成数据源和分库分表策略的初始化,策略初始化完成后缓存到内存中。

  2. 发起数据库操作指令时,解析是否需要分库分表(流程中只给出了需要分库分表的流程),需要则通过提取到的策略 key 获取对应的分库分表策略并进行分库分表,完成数据库操作。


通过上述的流程图可以看到,分库分表 SDK 通过 spring.factories 支持动态加载分库分表策略以兼容不同项目的不同使用场景。

其中分库分表部分的策略类图:

其中:ShardingStrategy 和 DBTableShardingStrategy 为接口;BaseShardingStrategy 为默认实现类;DefaultStrategy 和 CountryDbSwitchStrategy 为 SDK 中基于不同场景默认实现的分库分表策略。

在项目实际使用时,动态扩展的分库分表策略只需要继承 BaseShardingStrategy 即可,SDK 中初始化分库分表策略时通过 SpringFactoriesLoader.loadFactories()实现动态加载。

六、总结

SPI 技术将服务接口与服务实现分离以达到解耦,极大的提升程序的可扩展性。

本文重点介绍了 Java 内置 SPI 和 Dubbo SPI 以及 Spring SPI 三者的原理和相关源码;首先演示了三种 SPI 技术的实现,然后通过演示代码深入阅读了三种 SPI 的实现源码;其中重点介绍了 Spring SPI 的两种实现方式:spring.handlers 和 spring.factories,以及使用 spring.factories 实现的分库分表策略加载。希望通过阅读本文可以让读者对 SPI 有更深入的了解。

发布于: 18 小时前阅读数: 7
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020.07.10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
剖析 SPI 在 Spring 中的应用_spring_vivo互联网技术_InfoQ写作社区