关于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有更深刻的理解。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理