ToB企服应用市场:ToB评测及商务社交产业平台

标题: 剖析 SPI 在 Spring 中的应用 [打印本页]

作者: 钜形不锈钢水箱    时间: 2022-6-23 14:27
标题: 剖析 SPI 在 Spring 中的应用
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的实现
① 创建服务提供接口
  1. package jdk.spi;
  2. // 接口
  3. public interface DataBaseSPI {
  4.     public void dataBaseOperation();
  5. }
复制代码
② 创建服务提供接口的实现类
实现类1
  1. package jdk.spi.impl;
  2. import jdk.spi.DataBaseSPI;
  3. public class MysqlDataBaseSPIImpl implements DataBaseSPI {
  4.     @Override
  5.     public void dataBaseOperation() {
  6.         System.out.println("Operate Mysql database!!!");
  7.     }
  8. }
复制代码
实现类2
  1. package jdk.spi.impl;
  2. import jdk.spi.DataBaseSPI;
  3. public class OracleDataBaseSPIImpl implements DataBaseSPI {
  4.     @Override
  5.     public void dataBaseOperation() {
  6.         System.out.println("Operate Oracle database!!!");
  7.     }
  8. }
复制代码
③ 在项目META-INF/services/目录下创建jdk.spi.DataBaseSPI文件

jdk.spi.DataBaseSPI
  1. jdk.spi.impl.MysqlDataBaseSPIImpl
  2. jdk.spi.impl.OracleDataBaseSPIImpl
复制代码
④ 运行代码:
JdkSpiTest#main()
  1. package jdk.spi;
  2. import java.util.ServiceLoader;
  3. public class JdkSpiTest {
  4.     public static void main(String args[]){
  5.         // 加载jdk.spi.DataBaseSPI文件中DataBaseSPI的实现类(懒加载)
  6.         ServiceLoader<DataBaseSPI> dataBaseSpis = ServiceLoader.load(DataBaseSPI.class);
  7.         // ServiceLoader实现了Iterable,故此处可以使用for循环遍历加载到的实现类
  8.         for(DataBaseSPI spi : dataBaseSpis){
  9.             spi.dataBaseOperation();
  10.         }
  11.     }
  12. }
复制代码
⑤ 运行结果:
  1. Operate Mysql database!!!
  2. Operate Oracle database!!!
复制代码
2.2 源码分析

上述实现即为使用Java内置SPI实现的简单示例,ServiceLoader是Java内置的用于查找服务提供接口的工具类,通过调用load()方法实现对服务提供接口的查找(严格意义上此步并未真正的开始查找,只做初始化),最后遍历来逐个访问服务提供接口的实现类。
上述访问服务实现类的方式很不方便,如:无法直接使用某个服务,需要通过遍历来访问服务提供接口的各个实现,到此很多同学会有疑问:
在分析源码之前先给出答案:两个都是的;Java内置的SPI机制只能通过遍历的方式访问服务提供接口的实现类,而且服务提供接口的配置文件也只能放在META-INF/services/目录下。
ServiceLoader部分源码
  1. public final class ServiceLoader<S> implements Iterable<S>{
  2.     // 服务提供接口对应文件放置目录
  3.     private static final String PREFIX = "META-INF/services/";
  4.     // The class or interface representing the service being loaded
  5.     private final Class<S> service;
  6.     // 类加载器
  7.     private final ClassLoader loader;
  8.     // The access control context taken when the ServiceLoader is created
  9.     private final AccessControlContext acc;
  10.     // 按照初始化顺序缓存服务提供接口实例
  11.     private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  12.     // 内部类,实现了Iterator接口
  13.     private LazyIterator lookupIterator;
  14. }
复制代码
从源码中可以发现:
所以Java内置的SPI机制思想是非常好的,但其内置实现上的不足也很明显。
三、Dubbo SPI

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

① 扩展点
一个Java接口,等同于服务提供接口,需用@SPI注解修饰。
② 扩展
扩展点的实现类。
③ 扩展类加载器:ExtensionLoader
类似于Java SPI的ServiceLoader,主要用来加载并实例化扩展类。一个扩展点对应一个扩展加载器。
④ Dubbo扩展文件加载路径
Dubbo框架支持从以下三个路径来加载扩展类:
Dubbo框架针对三个不同路径下的扩展配置文件对应三个策略类:
三个路径下的扩展配置文件并没有特殊之处,一般情况下:
⑤ 扩展配置文件
和Java SPI不同,Dubbo的扩展配置文件中扩展类都有一个名称,便于在应用中引用它们。
如:Dubbo SPI扩展配置文件
  1. #扩展实例名称=扩展点实现类
  2. adaptive=org.apache.dubbo.common.compiler.support.AdaptiveCompiler
  3. jdk=org.apache.dubbo.common.compiler.support.JdkCompiler
  4. javassist=org.apache.dubbo.common.compiler.support.JavassistCompiler
复制代码
3.2 Dubbo SPI

先通过代码来演示下 Dubbo SPI 的实现。
① 创建扩展点(即服务提供接口)
扩展点
  1. package dubbo.spi;
  2. import org.apache.dubbo.common.extension.SPI;
  3. @SPI  // 注解标记当前接口为扩展点
  4. public interface DataBaseSPI {
  5.     public void dataBaseOperation();
  6. }
复制代码
② 创建扩展点实现类
扩展类1
  1. package dubbo.spi.impl;
  2. import dubbo.spi.DataBaseSPI;
  3. public class MysqlDataBaseSPIImpl implements DataBaseSPI {
  4.     @Override
  5.     public void dataBaseOperation() {
  6.         System.out.println("Dubbo SPI Operate Mysql database!!!");
  7.     }
  8. }
复制代码
扩展类2
  1. package dubbo.spi.impl;
  2. import dubbo.spi.DataBaseSPI;
  3. public class OracleDataBaseSPIImpl implements DataBaseSPI {
  4.     @Override
  5.     public void dataBaseOperation() {
  6.         System.out.println("Dubbo SPI Operate Oracle database!!!");
  7.     }
  8. }
复制代码
③在项目META-INF/dubbo/目录下创建dubbo.spi.DataBaseSPI文件:

dubbo.spi.DataBaseSPI
  1. #扩展实例名称=扩展点实现类
  2. mysql = dubbo.spi.impl.MysqlDataBaseSPIImpl
  3. oracle = dubbo.spi.impl.OracleDataBaseSPIImpl
复制代码
PS:文件内容中,等号左边为该扩展类对应的扩展实例名称,右边为扩展类(内容格式为一行一个扩展类,多个扩展类分为多行)
④ 运行代码:
DubboSpiTest#main()
  1. package dubbo.spi;
  2. import org.apache.dubbo.common.extension.ExtensionLoader;
  3. public class DubboSpiTest {
  4.     public static void main(String args[]){
  5.         // 使用扩展类加载器加载指定扩展的实现
  6.         ExtensionLoader<DataBaseSPI> dataBaseSpis = ExtensionLoader.getExtensionLoader(DataBaseSPI.class);
  7.         // 根据指定的名称加载扩展实例(与dubbo.spi.DataBaseSPI中一致)
  8.         DataBaseSPI spi = dataBaseSpis.getExtension("mysql");
  9.         spi.dataBaseOperation();
  10.          
  11.         DataBaseSPI spi2 = dataBaseSpis.getExtension("oracle");
  12.         spi2.dataBaseOperation();
  13.     }
  14. }
复制代码
⑤ 运行结果:
  1. Dubbo SPI Operate Mysql database!!!
  2. Dubbo SPI Operate Oracle database!!!
复制代码
从上面的代码实现直观来看,Dubbo SPI在使用上和Java SPI比较类似,但也有差异。
相同:
不同:
3.3 源码分析

以上述的代码实现作为源码分析入口,了解下Dubbo SPI是如何实现的。
ExtensionLoader
① 通过ExtensionLoader.getExtensionLoader(Classtype)创建对应扩展类型的扩展加载器。
ExtensionLoader#getExtensionLoader()
  1. public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
  2.     if (type == null) {
  3.         throw new IllegalArgumentException("Extension type == null");
  4.     }
  5.     // 校验当前类型是否为接口
  6.     if (!type.isInterface()) {
  7.         throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
  8.     }
  9.     // 接口上是否使用了@SPI注解
  10.     if (!withExtensionAnnotation(type)) {
  11.         throw new IllegalArgumentException("Extension type (" + type +
  12.                 ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
  13.     }
  14.     // 从内存中读取该扩展点的扩展类加载器
  15.     ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  16.     // 内存中不存在则直接new一个扩展
  17.     if (loader == null) {
  18.         EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
  19.         loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
  20.     }
  21.     return loader;
  22. }
复制代码
getExtensionLoader()方法中有三点比较重要的逻辑:
再看下new ExtensionLoader(type)源码
ExtensionLoader#ExtensionLoader()
  1. // 私有构造器
  2. private ExtensionLoader(Class<?> type) {
  3.      this.type = type;
  4.      // 创建ExtensionFactory自适应扩展
  5.      objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
  6. }
复制代码
重点:构造方法为私有类型,即外部无法直接使用构造方法创建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()
  1. public T getExtension(String name) {
  2.      // 调用两个参数的getExtension方法,默认true表示需要对扩展实例做包装
  3.      return getExtension(name, true);
  4. }
  5. public T getExtension(String name, boolean wrap) {
  6.     if (StringUtils.isEmpty(name)) {
  7.         throw new IllegalArgumentException("Extension name == null");
  8.     }
  9.     if ("true".equals(name)) {
  10.         return getDefaultExtension();
  11.     }
  12.     // 获取Holder实例,先从ConcurrentMap类型的内存缓存中取,没值会new一个并存放到内存缓存中
  13.     // Holder用来存放一个类型的值,这里用于存放扩展实例
  14.     final Holder<Object> holder = getOrCreateHolder(name);
  15.     // 从Holder读取该name对应的实例
  16.     Object instance = holder.get();
  17.     if (instance == null) {
  18.        // 同步控制
  19.        synchronized (holder) {
  20.           instance = holder.get();
  21.           // double check
  22.           if (instance == null) {
  23.              // 不存在扩展实例则解析扩展配置文件,实时创建
  24.              instance = createExtension(name, wrap);
  25.              holder.set(instance);
  26.           }
  27.         }
  28.      }
  29.      return (T) instance;
  30. }
复制代码
Holder类:这里用来存放指定扩展实例
③ 使用createExtension()创建扩展实例
ExtensionLoader#createExtension()
  1. // 部分createExtension代码
  2. private T createExtension(String name, boolean wrap) {
  3.    // 先调用getExtensionClasses()解析扩展配置文件,并生成内存缓存,
  4.    // 然后根据扩展实例名称获取对应的扩展类
  5.    Class<?> clazz = getExtensionClasses().get(name);
  6.    if (clazz == null) {
  7.        throw findException(name);
  8.    }
  9.    try {
  10.        // 根据扩展类生成实例并对实例做包装(主要是进行依赖注入和初始化)
  11.        // 优先从内存中获取该class类型的实例
  12.        T instance = (T) EXTENSION_INSTANCES.get(clazz);
  13.        if (instance == null) {
  14.            // 内存中不存在则直接初始化然后放到内存中
  15.            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
  16.            instance = (T) EXTENSION_INSTANCES.get(clazz);
  17.        }
  18.        // 主要是注入instance中的依赖
  19.        injectExtension(instance);
  20.        ......
  21. }
复制代码
createExtension()方法:创建扩展实例,方法中EXTENSION_INSTANCES为ConcurrentMap类型的内存缓存,先从内存中取,内存中不存在重新创建;其中一个核心方法是getExtensionClasses():
ExtensionLoader#getExtensionClasses()
  1. private Map<String, Class<?>> getExtensionClasses() {
  2.    // 优先从内存缓存中读
  3.     Map<String, Class<?>> classes = cachedClasses.get();
  4.     if (classes == null) {
  5.         // 采用同步手段解析配置文件
  6.         synchronized (cachedClasses) {
  7.             // double check
  8.             classes = cachedClasses.get();
  9.             if (classes == null) {
  10.                 // 正式开始解析配置文件
  11.                 classes = loadExtensionClasses();
  12.                 cachedClasses.set(classes);
  13.             }
  14.         }
  15.     }
  16.     return classes;
  17. }
复制代码
cachedClasses为Holder类型的内存缓存,getExtensionClasses中会优先读内存缓存,内存中不存在则采用同步的方式解析配置文件,最终在loadExtensionClasses方法中解析配置文件,完成从扩展配置文件中读出扩展类:
ExtensionLoader#loadExtensionClasses()
  1. // 在getExtensionClasses方法中是以同步的方式调用,是线程安全
  2. private Map<String, Class<?>> loadExtensionClasses() {
  3.    // 缓存默认扩展名称
  4.    cacheDefaultExtensionName();
  5.    Map<String, Class<?>> extensionClasses = new HashMap<>();
  6.    // strategies策略类集合,分别对应dubbo的三个配置文件目录
  7.    for (LoadingStrategy strategy : strategies) {
  8.       loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
  9.       loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(),
  10.            strategy.excludedPackages());
  11.    }
  12.    return extensionClasses;
  13. }
复制代码
源码中的strategies即static volatile LoadingStrategy[] strategies数组,通过Java SPI从META-INF/services/目录下加载配置文件完成初始化,默认包含三个类:
分别对应dubbo的三个目录:
上述的源码分析只是对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 基本概念

类似于Java SPI的ServiceLoader,负责解析spring.handlers配置文件,生成namespaceUri和NamespaceHandler名称的映射,并实例化NamespaceHandler。
自定义标签配置文件;Spring在2.0时便引入了spring.handlers,通过配置spring.handlers文件实现自定义标签并使用自定义标签解析类进行解析实现动态扩,内容配置如:
  1. http\://www.springframework.org/schema/c=org.springframework.beans.factory.xml.SimpleConstructorNamespaceHandler
  2. http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
  3. http\://www.springframework.org/schema/util=org.springframework.beans.factory.xml.UtilNamespaceHandler
  4. spring.handlers实现的SPI是以namespaceUri作为key,NamespaceHandler作为value,建立映射关系,在解析标签时通过namespaceUri获取相应的NamespaceHandler来解析
复制代码
类似于Java SPI的ServiceLoader,负责解析spring.factories,并将指定接口的所有实现类实例化后返回。
Spring在3.2时引入spring.factories,加强版的SPI配置文件,为Spring的SPI机制的实现提供支撑,内容配置如:
  1. # PropertySource Loaders
  2. org.springframework.boot.env.PropertySourceLoader=\
  3. org.springframework.boot.env.PropertiesPropertySourceLoader,\
  4. org.springframework.boot.env.YamlPropertySourceLoader
  5. # Run Listeners
  6. org.springframework.boot.SpringApplicationRunListener=\org.springframework.boot.context.event.EventPublishingRunListener
  7. 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
  1. package spring.spi.handlers;
  2. import org.springframework.beans.factory.config.BeanDefinition;
  3. import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
  4. import org.springframework.beans.factory.xml.ParserContext;
  5. import org.w3c.dom.Element;
  6. // 继承抽象类
  7. public class MysqlDataBaseHandler extends NamespaceHandlerSupport {
  8.     @Override
  9.     public void init() {   
  10.     }
  11.      
  12.     @Override
  13.     public BeanDefinition parse(Element element, ParserContext parserContext) {
  14.         System.out.println("MysqlDataBaseHandler!!!");
  15.         return null;
  16.     }
  17. }
复制代码
OracleDataBaseHandler
  1. package spring.spi.handlers;
  2. import org.springframework.beans.factory.config.BeanDefinition;
  3. import org.springframework.beans.factory.xml.NamespaceHandlerSupport;
  4. import org.springframework.beans.factory.xml.ParserContext;
  5. import org.w3c.dom.Element;
  6. public class OracleDataBaseHandler extends NamespaceHandlerSupport {
  7.     @Override
  8.     public void init() {
  9.     }
  10.     @Override
  11.     public BeanDefinition parse(Element element, ParserContext parserContext) {
  12.         System.out.println("OracleDataBaseHandler!!!");
  13.         return null;
  14.     }
  15. }
复制代码
② 在项目META-INF/目录下创建spring.handlers文件:

文件内容:
spring.handlers
  1. #一个namespaceUri对应一个handler
  2. http\://www.mysql.org/schema/mysql=spring.spi.handlers.MysqlDataBaseHandler
  3. http\://www.oracle.org/schema/oracle=spring.spi.handlers.OracleDataBaseHandler
复制代码
③ 运行代码:
SpringSpiTest#main()
  1. package spring.spi;
  2. import org.springframework.beans.factory.xml.DefaultNamespaceHandlerResolver;
  3. import org.springframework.beans.factory.xml.NamespaceHandler;
  4. public class SpringSpiTest {
  5.     public static void main(String args[]){
  6.         // spring中提供的默认namespace URI解析器
  7.         DefaultNamespaceHandlerResolver resolver = new DefaultNamespaceHandlerResolver();
  8.         // 此处假设nameSpaceUri已从xml文件中解析出来,正常流程是在项目启动的时候会解析xml文件,获取到对应的自定义标签
  9.         // 然后根据自定义标签取得对应的nameSpaceUri
  10.         String mysqlNameSpaceUri = "http://www.mysql.org/schema/mysql";
  11.         NamespaceHandler  handler = resolver.resolve(mysqlNameSpaceUri);
  12.         // 验证自定义NamespaceHandler,这里参数传null,实际使用中传具体的Element
  13.         handler.parse(null, null);
  14.          
  15.         String oracleNameSpaceUri = "http://www.oracle.org/schema/oracle";
  16.         handler = resolver.resolve(oracleNameSpaceUri);
  17.         handler.parse(null, null);
  18.     }
  19. }
复制代码
④ 运行结果:
  1. MysqlDataBaseHandler!!!
  2. OracleDataBaseHandler!!!
复制代码
上述代码通过解析spring.handlers实现对自定义标签的动态解析,以NameSpaceURI作为key获取具体的NameSpaceHandler实现类,这里有别于Java SPI,其中:
DefaultNamespaceHandlerResolver是NamespaceHandlerResolver接口的默认实现类,用于解析自定义标签。
4.2.2 源码分析

下面从上述代码开始深入源码了解spring handlers方式实现的SPI是如何工作的。
① DefaultNamespaceHandlerResolver.resolve()方法本身是根据namespaceUri获取对应的namespaceHandler对标签进行解析,核心源码:
DefaultNamespaceHandlerResolver#resolve()
  1. public NamespaceHandler resolve(String namespaceUri) {
  2.     // 1、核心逻辑之一:获取namespaceUri和namespaceHandler映射关系
  3.     Map<String, Object> handlerMappings = getHandlerMappings();
  4.     // 根据namespaceUri参数取对应的namespaceHandler全限定类名or NamespaceHandler实例
  5.     Object handlerOrClassName = handlerMappings.get(namespaceUri);
  6.     if (handlerOrClassName == null) {
  7.         return null;
  8.     }
  9.     // 2、handlerOrClassName是已初始化过的实例则直接返回
  10.     else if (handlerOrClassName instanceof NamespaceHandler) {
  11.         return (NamespaceHandler) handlerOrClassName;
  12.     }else {
  13.         String className = (String) handlerOrClassName;
  14.         try {
  15.             ///3、使用反射根据namespaceHandler全限定类名加载实现类
  16.             Class<?> handlerClass = ClassUtils.forName(className, this.classLoader);
  17.             if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) {
  18.                 throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri +
  19.                         "] does not implement the [" + NamespaceHandler.class.getName() + "] interface");
  20.             }
  21.             // 3.1、初始化namespaceHandler实例
  22.             NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass);
  23.             // 3.2、 初始化,不同的namespaceHandler实现类初始化方法逻辑有差异
  24.             namespaceHandler.init();
  25.             // 4、将初始化好的实例放入内存缓存中,下次解析到相同namespaceUri标签时直接返回,避免再次初始化
  26.             handlerMappings.put(namespaceUri, namespaceHandler);
  27.             return namespaceHandler;
  28.         }catch (ClassNotFoundException ex) {
  29.             throw new FatalBeanException("NamespaceHandler class [" + className + "] for namespace [" +
  30.                     namespaceUri + "] not found", ex);
  31.         }catch (LinkageError err) {
  32.             throw new FatalBeanException("Invalid NamespaceHandler class [" + className + "] for namespace [" +
  33.                     namespaceUri + "]: problem with handler class file or dependent class", err);
  34.         }
  35.     }
  36. }
复制代码
第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()
  1. private Map<String, Object> getHandlerMappings() {
  2.     Map<String, Object> handlerMappings = this.handlerMappings;
  3.     // 使用线程安全的解析逻辑,避免在并发场景下重复的解析,没必要重复解析
  4.     // 这里在同步代码块的内外对handlerMappings == null作两次判断很有必要,采用懒汉式初始化
  5.     if (handlerMappings == null) {
  6.         synchronized (this) {
  7.             handlerMappings = this.handlerMappings;
  8.             // duble check
  9.             if (handlerMappings == null) {
  10.                 if (logger.isDebugEnabled()) {
  11.                     logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
  12.                 }
  13.                 try {
  14.                     // 加载handlerMappingsLocation目录文件,handlerMappingsLocation路径值可变,默认是META-INF/spring.handlers
  15.                     Properties mappings =
  16.                             PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
  17.                     if (logger.isDebugEnabled()) {
  18.                         logger.debug("Loaded NamespaceHandler mappings: " + mappings);
  19.                     }
  20.                     // 初始化内存缓存
  21.                     handlerMappings = new ConcurrentHashMap<String, Object>(mappings.size());
  22.                     // 将加载到的属性合并到handlerMappings中
  23.                     CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
  24.                     // 赋值内存缓存
  25.                     this.handlerMappings = handlerMappings;
  26.                 }catch (IOException ex) {
  27.                     throw new IllegalStateException(
  28.                             "Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
  29.                 }
  30.             }
  31.         }
  32.     }
  33.     return handlerMappings;
  34. }
复制代码
源码中this.handlerMappings是一个Map类型的内存缓存,存放解析到的namespaceUri以及NameSpaceHandler实例。
getHandlerMappings()方法体中的实现使用了线程安全方式,增加了同步逻辑。
通过阅读源码可以了解到Spring基于spring.handlers实现SPI逻辑相对比较简单,但应用却比较灵活,对自定义标签的支持很方便,在不修改Spring源码的前提下轻松实现接入,如Dubbo中定义的各种Dubbo标签便是很好的利用了spring.handlers。
Spring提供如此灵活的功能,那是如何应用的呢?下面简单了解下parseCustomElement()。
resolve作为工具类型的方法,被使用的地方比较多,这里仅简单介绍在BeanDefinitionParserDelegate.parseCustomElement()中的应用。
BeanDefinitionParserDelegate#parseCustomElement()
  1. public BeanDefinition parseCustomElement(Element ele, BeanDefinition containingBd) {
  2.      // 获取标签的namespaceUri
  3.      String namespaceUri = getNamespaceURI(ele);
  4.      // 首先获得DefaultNamespaceHandlerResolver实例在再以namespaceUri作为参数调用resolve方法解析取得NamespaceHandler
  5.      NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri);
  6.      if (handler == null) {
  7.          error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele);
  8.          return null;
  9.      }
  10.      // 调用NamespaceHandler中的parse方法开始解析标签
  11.      return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd));
  12. }
复制代码
parseCustomElement作为解析标签的中间方法,再看下parseCustomElement的调用情况:

在parseBeanDefinitions()中被调用,再看下parseBeanDefinitions的源码
DefaultBeanDefinitionDocumentReader#parseBeanDefinitions()
  1. protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) {
  2.     // spring内部定义的标签为默认标签,即非spring内部定义的标签都不是默认的namespace
  3.     if (delegate.isDefaultNamespace(root)) {
  4.         NodeList nl = root.getChildNodes();
  5.         for (int i = 0; i < nl.getLength(); i++) {
  6.             Node node = nl.item(i);
  7.             if (node instanceof Element) {
  8.                 Element ele = (Element) node;
  9.                 // root子标签也做此判断
  10.                 if (delegate.isDefaultNamespace(ele)) {
  11.                     parseDefaultElement(ele, delegate);
  12.                 }else{
  13.                     // 子标签非spring默认标签(即自定义标签)也走parseCustomElement来解析
  14.                     delegate.parseCustomElement(ele);
  15.                 }
  16.             }
  17.         }
  18.     }else {
  19.         // 非spring的默认标签(即自定义的标签)走parseCustomElement来解析
  20.         delegate.parseCustomElement(root);
  21.     }
  22. }
复制代码
到此就很清晰了,调用前判断是否为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接口
接口
  1. package spring.spi.factories;
  2. public interface DataBaseSPI {
  3.     public void dataBaseOperation();
  4. }
复制代码
② 创建DataBaseSPI接口的实现类
MysqlDataBaseImpl
  1. #实现类1
  2. package spring.spi.factories.impl;
  3. import spring.spi.factories.DataBaseSPI;
  4. public class MysqlDataBaseImpl implements DataBaseSPI {
  5.     @Override
  6.     public void dataBaseOperation() {
  7.         System.out.println("Mysql database test!!!!");
  8.     }
  9. }
复制代码
MysqlDataBaseImpl
  1. #实现类2
  2. package spring.spi.factories.impl;
  3. import spring.spi.factories.DataBaseSPI;
  4. public class OracleDataBaseImpl implements DataBaseSPI {
  5.     @Override
  6.     public void dataBaseOperation() {
  7.         System.out.println("Oracle database test!!!!");
  8.     }
  9. }
复制代码
③ 在项目META-INF/目录下创建spring.factories文件:

文件内容
spring.factories
  1. #key是接口的全限定名,value是接口的实现类
  2. spring.spi.factories.DataBaseSPI = spring.spi.factories.impl.MysqlDataBaseImpl,spring.spi.factories.impl.OracleDataBaseImpl
复制代码
④ 运行代码
SpringSpiTest#main()
  1. package spring.spi.factories;
  2. import java.util.List;
  3. import org.springframework.core.io.support.SpringFactoriesLoader;
  4. public class SpringSpiTest {
  5.     public static void main(String args[]){
  6.          
  7.         // 调用SpringFactoriesLoader.loadFactories方法加载DataBaseSPI接口所有实现类的实例
  8.         List<DataBaseSPI> spis= SpringFactoriesLoader.loadFactories(DataBaseSPI.class, Thread.currentThread().getContextClassLoader());
  9.          
  10.         // 遍历DataBaseSPI接口实现类实例
  11.         for(DataBaseSPI spi : spis){
  12.             spi.dataBaseOperation();
  13.         }
  14.     }
  15. }
复制代码
⑤ 运行结果
  1. Mysql database test!!!!
  2. 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#loadFactories()
  1. public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
  2.     Assert.notNull(factoryClass, "'factoryClass' must not be null");
  3.     ClassLoader classLoaderToUse = classLoader;
  4.     // 1.确定类加载器
  5.     if (classLoaderToUse == null) {
  6.         classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
  7.     }
  8.     // 2.核心逻辑之一:解析各jar包中META-INF/spring.factories文件中factoryClass的实现类全限定名
  9.     List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
  10.     if (logger.isTraceEnabled()) {
  11.         logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
  12.     }
  13.     List<T> result = new ArrayList<T>(factoryNames.size());
  14.     // 3.遍历实现类的全限定名并进行实例化
  15.     for (String factoryName : factoryNames) {
  16.         result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
  17.     }
  18.     // 排序
  19.     AnnotationAwareOrderComparator.sort(result);
  20.     // 4.返回实例化后的结果集
  21.     return result;
  22. }
复制代码
源码中loadFactoryNames() 是另外一个比较核心的方法,解析spring.factories文件中指定接口的实现类的全限定名,实现逻辑见后续的源码。
经过源码中第2步解析得到实现类的全限定名后,在第3步通过instantiateFactory()方法逐个实例化实现类。
再看loadFactoryNames()源码是如何解析得到实现类全限定名的:
SpringFactoriesLoader#loadFactoryNames()
  1. public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
  2.     // 1.接口全限定名
  3.     String factoryClassName = factoryClass.getName();
  4.     try {
  5.         // 2.加载META-INF/spring.factories文件路径(分布在各个不同jar包里,所以这里会是多个文件路径,枚举返回)
  6.         Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
  7.                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
  8.         List<String> result = new ArrayList<String>();
  9.         // 3.遍历枚举集合,逐个解析spring.factories文件
  10.         while (urls.hasMoreElements()) {
  11.             URL url = urls.nextElement();
  12.             Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
  13.             String propertyValue = properties.getProperty(factoryClassName);
  14.             // 4.spring.factories文件中一个接口的实现类有多个时会用逗号隔开,这里拆开获取实现类全限定名
  15.             for (String factoryName : StringUtils.commaDelimitedListToStringArray(propertyValue)) {
  16.                 result.add(factoryName.trim());
  17.             }
  18.         }
  19.         return result;
  20.     }catch (IOException ex) {
  21.         throw new IllegalArgumentException("Unable to load factories from location [" +
  22.                 FACTORIES_RESOURCE_LOCATION + "]", ex);
  23.     }
  24. }
复制代码
源码中第2步获取所有jar包中META-INF/spring.factories文件路径,以枚举值返回。
源码中第3步开始遍历spring.factories文件路径,逐个加载解析,整合factoryClass类型的实现类名称。
获取到实现类的全限定名集合后,便根据实现类的名称逐个实例化,继续看下instantiateFactory()方法的源码:
SpringFactoriesLoader#instantiateFactory()
  1. private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
  2.     try {
  3.         // 1.使用classLoader类加载器加载instanceClassName类
  4.         Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
  5.         if (!factoryClass.isAssignableFrom(instanceClass)) {
  6.             throw new IllegalArgumentException(
  7.                     "Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
  8.         }
  9.         // 2.instanceClassName类中的构造方法
  10.         Constructor<?> constructor = instanceClass.getDeclaredConstructor();
  11.         ReflectionUtils.makeAccessible(constructor);
  12.         // 3.实例化
  13.         return (T) constructor.newInstance();
  14.     }
  15.     catch (Throwable ex) {
  16.         throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), ex);
  17.     }
  18. }
复制代码
实例化方法是私有型(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中的调用。
SpringApplication#getSpringFactoriesInstance()
  1. // 单个参数getSpringFactoriesInstances方法
  2. private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
  3.     // 默认调用多参的重载方法
  4.     return getSpringFactoriesInstances(type, new Class<?>[] {});
  5. }
  6. // 多个参数的getSpringFactoriesInstances方法
  7. private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
  8.         Class<?>[] parameterTypes, Object... args) {
  9.     ClassLoader classLoader = getClassLoader();
  10.     // 调用SpringFactoriesLoader中的loadFactoryNames方法加载接口实现类的全限定名
  11.     Set<String> names = new LinkedHashSet<>(
  12.             SpringFactoriesLoader.loadFactoryNames(type, classLoader));
  13.     // 实例化
  14.     List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
  15.             classLoader, args, names);
  16.     AnnotationAwareOrderComparator.sort(instances);
  17.     return instances;
  18. }
复制代码
在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的地方进行详细分析。

说明:
通过上述的流程图可以看到,分库分表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有更深入的了解。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4