乐趣区

关于spring:剖析-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 的实现

① 创立服务提供接口

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.MysqlDataBaseSPIImpl
jdk.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.AdaptiveCompiler
jdk=org.apache.dubbo.common.compiler.support.JdkCompiler
javassist=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.MysqlDataBaseSPIImpl
oracle = 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.SimpleConstructorNamespaceHandler
http\://www.springframework.org/schema/p=org.springframework.beans.factory.xml.SimplePropertyNamespaceHandler
http\://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 Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
 
# Run Listeners
org.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 对应一个 handler
http\://www.mysql.org/schema/mysql=spring.spi.handlers.MysqlDataBaseHandler
http\://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

# 实现类 1
package 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

# 实现类 2
package 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 有更深刻的理解。

退出移动版