作者:vivo 互联网服务器团队- Zhang Peng

SPI 是一种用于动态加载服务的机制。它的中心思想便是解耦,归于典型的微内核架构形式。SPI 在 Java 国际运用十分广泛,如:Dubbo、Spring Boot 等结构。本文从源码下手分析,深入探讨 Java SPI 的特性、原理,以及在一些比较经典领域的运用。

一、SPI 简介

SPI 全称 Service Provider Interface,是 Java 供给的,旨在由第三方完结或扩展的 API,它是一种用于动态加载服务的机制。Java 中 SPI 机制首要思想是将安装的操控权移到程序之外,在模块化规划中这个机制尤其重要,其中心思想便是 解耦。

Java SPI 有四个要素:

  • **SPI 接口:**为服务供给者完结类约好的的接口或抽象类。

  • **SPI 完结类:**实践供给服务的完结类。

  • **SPI 装备:**Java SPI 机制约好的装备文件,供给查找服务完结类的逻辑。装备文件有必要置于 META-INF/services 目录中,而且,文件名应与服务供给者接口的彻底约束名保持一致。文件中的每一行都有一个完结服务类的详细信息,相同是服务供给者类的彻底约束称号。

  • **ServiceLoader:**Java SPI 的中心类,用于加载 SPI 完结类。ServiceLoader 中有各种有用办法来获取特定完结、迭代它们或重新加载服务。

二、SPI 示例

正所谓,实践出真知,咱们无妨经过一个详细的示例来看一下,怎么运用 Java SPI。

2.1 SPI 接口

首要,需求界说一个 SPI 接口,和一般接口并没有什么不同。

package io.github.dunwu.javacore.spi;
public interface DataStorage {
    String search(String key);
}

2.2 SPI 完结类

假设,咱们需求在程序中运用两种不同的数据存储——MySQL 和 Redis。因而,咱们需求两个不同的完结类去分别完结相应作业。

MySQL查询 MOCK 类

package io.github.dunwu.javacore.spi;
public class MysqlStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "【Mysql】查找" + key + ",成果:No";
    }
}

Redis 查询 MOCK 类

package io.github.dunwu.javacore.spi;
public class RedisStorage implements DataStorage {
    @Override
    public String search(String key) {
        return "【Redis】查找" + key + ",成果:Yes";
    }
}

service 传入的是希望加载的 SPI 接口类型 到目前为止,界说接口,并完结接口和一般的 Java 接口完结没有任何不同。

2.3 SPI 装备

假如想经过 Java SPI 机制来发现服务,就需求在 SPI 装备中约好好发现服务的逻辑。装备文件有必要置于 META-INF/services 目录中,而且,文件名应与服务供给者接口的彻底约束名保持一致。文件中的每一行都有一个完结服务类的详细信息,相同是服务供给者类的彻底约束称号。以本示例代码为例,其文件名应该为io.github.dunwu.javacore.spi.DataStorage,

文件中的内容如下:

io.github.dunwu.javacore.spi.MysqlStorage
io.github.dunwu.javacore.spi.RedisStorage

2.4 ServiceLoader

完结了上面的进程,就能够经过 ServiceLoader 来加载服务。示例如下:

import java.util.ServiceLoader;
public class SpiDemo {
    public static void main(String[] args) {
        ServiceLoader<DataStorage> serviceLoader = ServiceLoader.load(DataStorage.class);
        System.out.println("============ Java SPI 测试============");
        serviceLoader.forEach(loader -> System.out.println(loader.search("Yes Or No")));
    }
}

输出:

============ Java SPI 测试============
【Mysql】查找Yes Or No,成果:No
【Redis】查找Yes Or No,成果:Yes

三、SPI 原理

上文中,咱们现已了解 Java SPI 的要素以及运用 Java SPI 的办法。你有没有想过,Java SPI 和一般 Java 接口有何不同,Java SPI 是怎么作业的。实践上,Java SPI 机制依靠于 ServiceLoader 类去解析、加载服务。因而,把握了 ServiceLoader 的作业流程,就把握了 SPI 的原理。ServiceLoader 的代码本身很精练,接下来,让咱们经过走读源码的办法,逐一理解 ServiceLoader 的作业流程。

3.1 ServiceLoader 的成员变量

先看一下 ServiceLoader 类的成员变量,大致有个印象,后边的源码中都会运用到。

public final class ServiceLoader<S> implements Iterable<S> {
    // SPI 装备文件目录
    private static final String PREFIX = "META-INF/services/";
    // 将要被加载的 SPI 服务
    private final Class<S> service;
    // 用于加载 SPI 服务的类加载器
    private final ClassLoader loader;
    // ServiceLoader 创立时的访问操控上下文
    private final AccessControlContext acc;
    // SPI 服务缓存,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 懒查询迭代器
    private LazyIterator lookupIterator;
    // ...
}

3.2 ServiceLoader 的作业流程

(1)ServiceLoader.load 静态办法

运用程序加载 Java SPI 服务,都是先调用 ServiceLoader.load 静态办法。

ServiceLoader.load 静态办法的效果是:

① 指定类加载 ClassLoader 和访问操控上下文;

② 然后,重新加载 SPI 服务

  • 清空缓存中一切已实例化的 SPI 服务

  • 依据 ClassLoader 和 SPI 类型,创立懒加载迭代器

这儿,摘抄 ServiceLoader.load 相关源码,如下:

// service 传入的是希望加载的 SPI 接口类型
// loader 是用于加载 SPI 服务的类加载器
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader) {
  return new ServiceLoader<>(service, loader);
}
public void reload() {
    // 清空缓存中一切已实例化的 SPI 服务
  providers.clear();
    // 依据 ClassLoader 和 SPI 类型,创立懒加载迭代器
  lookupIterator = new LazyIterator(service, loader);
}
// 私有结构办法
// 重新加载 SPI 服务
private ServiceLoader(Class<S> svc, ClassLoader cl) {
  service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 指定类加载 ClassLoader 和访问操控上下文
  loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
  acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    // 然后,重新加载 SPI 服务
  reload();
}

(2)运用程序经过 ServiceLoader 的 iterator 办法遍历 SPI 实例

ServiceLoader 的类界说,清晰了 ServiceLoader 类完结了 Iterable 接口,所以,它是能够迭代遍历的。实践上,ServiceLoader 类保护了一个缓存 providers( LinkedHashMap 方针),缓存 providers 中保存了现已被成功加载的 SPI 实例,这个 Map 的 key 是 SPI 接口完结类的全约束名,value 是该完结类的一个实例方针。

当运用程序调用 ServiceLoader 的 iterator 办法时,ServiceLoader 会先判断缓存 providers 中是否有数据:假如有,则直接回来缓存 providers 的迭代器;假如没有,则回来懒加载迭代器的迭代器。

public Iterator<S> iterator() {
  return new Iterator<S>() {
        // 缓存 SPI providers
    Iterator<Map.Entry<String,S>> knownProviders
      = providers.entrySet().iterator();
        // lookupIterator 是 LazyIterator 实例,用于懒加载 SPI 实例
    public boolean hasNext() {
      if (knownProviders.hasNext())
        return true;
      return lookupIterator.hasNext();
    }
    public S next() {
      if (knownProviders.hasNext())
        return knownProviders.next().getValue();
      return lookupIterator.next();
    }
    public void remove() {
      throw new UnsupportedOperationException();
    }
  };
}

(3)懒加载迭代器的作业流程

上面的源码中提到了,lookupIterator 是 LazyIterator 实例,而 LazyIterator 用于懒加载 SPI 实例。那么, LazyIterator 是怎么作业的呢?

这儿,摘取 LazyIterator 要害代码

hasNextService 办法:

  • 拼接 META-INF/services/ + SPI 接口全约束名

  • 经过类加载器,测验加载资源文件

  • 解析资源文件中的内容,获取 SPI 接口的完结类的全约束名 nextName

nextService 办法:

  • hasNextService() 办法解分出了 SPI 完结类的的全约束名 nextName,经过反射,获取 SPI 完结类的类界说 Class。

  • 然后,测验经过 Class 的 newInstance 办法实例化一个 SPI 服务方针。假如成功,则将这个方针加入到缓存 providers 中并回来该方针。

    private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 1.拼接 META-INF/services/ + SPI 接口全约束名 // 2.经过类加载器,测验加载资源文件 // 3.解析资源文件中的内容 String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, “Error locating configuration files”, x); } } while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } nextName = pending.next(); return true; }

    private S nextService() { if (!hasNextService()) throw new NoSuchElementException(); String cn = nextName; nextName = null; Class<?> c = null; try { c = Class.forName(cn, false, loader); } catch (ClassNotFoundException x) { fail(service, “Provider ” + cn + ” not found”); } if (!service.isAssignableFrom(c)) { fail(service, “Provider ” + cn + ” not a s”); } try { S p = service.cast(c.newInstance()); providers.put(cn, p); return p; } catch (Throwable x) { fail(service, “Provider ” + cn + ” could not be instantiated”, x); } throw new Error(); // This cannot happen }

3.3 SPI 和类加载器

经过上面两个章节中,走读 ServiceLoader 代码,咱们现已大致了解 Java SPI 的作业原理,即经过 ClassLoader 加载 SPI 装备文件,解析 SPI 服务,然后经过反射,实例化 SPI 服务实例。咱们无妨思考一下,为什么加载 SPI 服务时,需求指定类加载器 ClassLoader 呢?

学习过 JVM 的读者,想必都了解过类加载器的双亲派遣模型(Parents Delegation Model)。双亲派遣模型要求除了顶层的 BootstrapClassLoader 外,其他的类加载器都应有自己的父类加载器。这儿类加载器之间的父子联系一般经过组合(Composition)联系来完结,而不是经过承继(Inheritance)的联系完结。

双亲派遣机制约好了:一个类加载器首要将类加载恳求传送到父类加载器,只有当父类加载器无法完结类加载恳求时才测验加载。

**双亲派遣的好处:**使得 Java 类伴随着它的类加载器,天然具备一种带有优先级的层次联系,从而使得类加载得到一致,不会出现重复加载的问题:

  1. 系统类避免内存中出现多份相同的字节码

  2. 保证 Java 程序安全安稳运转

例如:java.lang.Object 存放在 rt.jar 中,假如编写别的一个 java.lang.Object 的类并放到 classpath 中,程序能够编译经过。由于双亲派遣模型的存在,所以在 rt.jar 中的 Object 比在 classpath 中的 Object 优先级更高,由于 rt.jar 中的 Object 运用的是发动类加载器,而 classpath 中的 Object 运用的是运用程序类加载器。正由于 rt.jar 中的 Object 优先级更高,由于程序中一切的 Object 都是这个 Object。

**双亲派遣的约束:**子类加载器能够运用父类加载器现已加载的类,而父类加载器无法运用子类加载器现已加载的。——这就导致了双亲派遣模型并不能解决一切的类加载器问题。Java SPI 就面临着这样的问题:

  • SPI 的接口是 Java 中心库的一部分,是由 BootstrapClassLoader 加载的;

  • 而 SPI 完结的 Java 类一般是由 AppClassLoader 来加载的。BootstrapClassLoader 是无法找到 SPI 的完结类的,由于它只加载 Java 的中心库。它也不能署理给 AppClassLoader,由于它是最顶层的类加载器。这也解释了本节开端的问题——为什么加载 SPI 服务时,需求指定类加载器 ClassLoader 呢?由于假如不指定 ClassLoader,则无法获取 SPI 服务。

假如不做任何的设置,Java 运用的线程的上下文类加载器默许便是 AppClassLoader。在中心类库运用 SPI 接口时,传递的类加载器运用线程上下文类加载器,就能够成功的加载到 SPI 完结的类。线程上下文类加载器在许多 SPI 的完结中都会用到。

一般能够经过Thread.currentThread().getClassLoader()和 Thread.currentThread().getContextClassLoader() 获取线程上下文类加载器。

3.4 Java SPI 的缺乏

Java SPI 存在一些缺乏:

  • 不能按需加载,需求遍历一切的完结,并实例化,然后在循环中才干找到咱们需求的完结。假如不想用某些完结类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了糟蹋。

  • 获取某个完结类的办法不够灵敏,只能经过 Iterator 办法获取,不能依据某个参数来获取对应的完结类。

  • 多个并发多线程运用 ServiceLoader 类的实例是不安全的。

四、SPI 运用场景

SPI 在 Java 开发中运用十分广泛。首要,在 Java 的 java.util.spi package 中就约好了许多 SPI 接口。下面,罗列一些 SPI 接口:

  • TimeZoneNameProvider: 为 TimeZone 类供给本地化的时区称号。

  • DateFormatProvider: 为指定的言语环境供给日期和时间格局。

  • NumberFormatProvider: 为 NumberFormat 类供给货币、整数和百分比值。

  • Driver: 从 4.0 版开端,JDBC API 支撑 SPI 形式。旧版本运用 Class.forName() 办法加载驱动程序。

  • PersistenceProvider: 供给 JPA API 的完结。

  • 等等

除此以外,SPI 还有许多运用,下面罗列几个经典事例。

4.1 SPI 运用事例之 JDBC DriverManager

作为 Java 工程师,尤其是 CRUD 工程师,相必都十分熟悉 JDBC。众所周知,联系型数据库有许多种,如:MySQL、Oracle、PostgreSQL 等等。JDBC 怎么识别各种数据库的驱动呢?

4.1.1 创立数据库衔接

咱们先回顾一下,JDBC 怎么创立数据库衔接的呢?

在 JDBC4.0 之前,衔接数据库的时候,一般会用 Class.forName(XXX) 办法来加载数据库相应的驱动,然后再获取数据库衔接,继而进行 CRUD 等操作。

Class.forName("com.mysql.jdbc.Driver")

而 JDBC4.0 之后,不再需求用Class.forName(XXX) 办法来加载数据库驱动,直接获取衔接就能够了。明显,这种办法很方便,可是怎么做到的呢?

**(1)JDBC 接口:**首要,Java 中内置了接口 java.sql.Driver。

**(2)JDBC 接口完结:**各个数据库的驱动自行完结 java.sql.Driver 接口,用于管理数据库衔接。

  • MySQL:在 MySQL的 Java 驱动包 mysql-connector-java-XXX.jar 中,能够找到 META-INF/services 目录,该目录下会有一个姓名为java.sql.Driver 的文件,文件内容是com.mysql.cj.jdbc.Driver。

com.mysql.cj.jdbc.Driver 正是 MySQL 版的 java.sql.Driver 完结。如下图所示:

源码级深度理解 Java SPI

  • PostgreSQL 完结:在 PostgreSQL 的 Java 驱动包 postgresql-42.0.0.jar 中,也能够找到相同的装备文件,文件内容是 org.postgresql.Driver,org.postgresql.Driver 正是 PostgreSQL 版的 java.sql.Driver 完结。

(3)创立数据库衔接

以 MySQL 为例,创立数据库衔接代码如下:

final String DB_URL = String.format("jdbc:mysql://%s:%s/%s", DB_HOST, DB_PORT, DB_SCHEMA);
connection = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD);

4.1.2 DriverManager

从前文,咱们现已知道 DriverManager 是创立数据库衔接的要害。它究竟是怎么作业的呢?

能够看到是加载实例化驱动的,接着看 loadInitialDrivers 办法:

private static void loadInitialDrivers() {
  String drivers;
  try {
    drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
      public String run() {
        return System.getProperty("jdbc.drivers");
      }
    });
  } catch (Exception ex) {
    drivers = null;
  }
  // 经过 classloader 获取一切完结 java.sql.Driver 的驱动类
  AccessController.doPrivileged(new PrivilegedAction<Void>() {
    public Void run() {
            // 使用 SPI,记载一切 Driver 服务
      ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
            // 获取迭代器
      Iterator<Driver> driversIterator = loadedDrivers.iterator();
      try{
                // 遍历迭代器
        while(driversIterator.hasNext()) {
          driversIterator.next();
        }
      } catch(Throwable t) {
      // Do nothing
      }
      return null;
    }
  });
    // 打印数据库驱动信息
  println("DriverManager.initialize: jdbc.drivers = " + drivers);
  if (drivers == null || drivers.equals("")) {
    return;
  }
  String[] driversList = drivers.split(":");
  println("number of Drivers:" + driversList.length);
  for (String aDriver : driversList) {
    try {
      println("DriverManager.Initialize: loading " + aDriver);
            // 测验实例化驱动
      Class.forName(aDriver, true,
          ClassLoader.getSystemClassLoader());
    } catch (Exception ex) {
      println("DriverManager.Initialize: load failed: " + ex);
    }
  }
}

上面的代码首要进程是:

  1. 从系统变量中获取驱动的完结类。

  2. 使用 SPI 来获取一切驱动的完结类。

  3. 遍历一切驱动,测验实例化各个完结类。

  4. 依据第 1 步获取到的驱动列表来实例化详细的完结类。

需求重视的是下面这行代码:

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);

这儿实践获取的是java.util.ServiceLoader.LazyIterator 迭代器。调用其 hasNext 办法时,会查找 classpath 下以及 jar 包中的 META-INF/services 目录,查找 java.sql.Driver 文件,并找到文件中的驱动完结类的全约束名。调用其 next 办法时,会依据驱动类的全约束名去测验实例化一个驱动类的方针。

4.2 SPI 运用事例之 Common-Loggin

common-logging(也称 Jakarta Commons Logging,缩写 JCL)是常用的日志门面工具包。common-logging 的中心类是进口是 LogFactory,LogFatory 是一个抽象类,它担任加载详细的日志完结。

其进口办法是 LogFactory.getLog 办法,源码如下:

public static Log getLog(Class clazz) throws LogConfigurationException {
  return getFactory().getInstance(clazz);
}
public static Log getLog(String name) throws LogConfigurationException {
  return getFactory().getInstance(name);
}

从以上源码可知,getLog 采用了工厂规划形式,是先调用 getFactory 办法获取详细日志库的工厂类,然后依据类称号或类型创立日志实例。

LogFatory.getFactory 办法担任选出匹配的日志工厂,其源码如下:

public static LogFactory getFactory() throws LogConfigurationException {
  // 省掉...
  // 加载 commons-logging.properties 装备文件
  Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
  // 省掉...
    // 决定创立哪个 LogFactory 实例
  // (1)测验读取全局特点 org.apache.commons.logging.LogFactory
  if (isDiagnosticsEnabled()) {
    logDiagnostic("[LOOKUP] Looking for system property [" + FACTORY_PROPERTY +
            "] to define the LogFactory subclass to use...");
  }
  try {
        // 假如指定了 org.apache.commons.logging.LogFactory 特点,测验实例化详细完结类
    String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
    if (factoryClass != null) {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] Creating an instance of LogFactory class '" + factoryClass +
                "' as specified by system property " + FACTORY_PROPERTY);
      }
      factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
    } else {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] No system property [" + FACTORY_PROPERTY + "] defined.");
      }
    }
  } catch (SecurityException e) {
      // 反常处理
  } catch (RuntimeException e) {
      // 反常处理
  }
    // (2)使用 Java SPI 机制,测验在 classpatch 的 META-INF/services 目录下寻觅 org.apache.commons.logging.LogFactory 完结类
  if (factory == null) {
    if (isDiagnosticsEnabled()) {
      logDiagnostic("[LOOKUP] Looking for a resource file of name [" + SERVICE_ID +
              "] to define the LogFactory subclass to use...");
    }
    try {
      final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
      if( is != null ) {
        // This code is needed by EBCDIC and other strange systems.
        // It's a fix for bugs reported in xerces
        BufferedReader rd;
        try {
          rd = new BufferedReader(new InputStreamReader(is, "UTF-8"));
        } catch (java.io.UnsupportedEncodingException e) {
          rd = new BufferedReader(new InputStreamReader(is));
        }
        String factoryClassName = rd.readLine();
        rd.close();
        if (factoryClassName != null && ! "".equals(factoryClassName)) {
          if (isDiagnosticsEnabled()) {
            logDiagnostic("[LOOKUP]  Creating an instance of LogFactory class " +
                    factoryClassName +
                    " as specified by file '" + SERVICE_ID +
                    "' which was present in the path of the context classloader.");
          }
          factory = newFactory(factoryClassName, baseClassLoader, contextClassLoader );
        }
      } else {
        // is == null
        if (isDiagnosticsEnabled()) {
          logDiagnostic("[LOOKUP] No resource file with name '" + SERVICE_ID + "' found.");
        }
      }
    } catch (Exception ex) {
      // note: if the specified LogFactory class wasn't compatible with LogFactory
      // for some reason, a ClassCastException will be caught here, and attempts will
      // continue to find a compatible class.
      if (isDiagnosticsEnabled()) {
        logDiagnostic(
          "[LOOKUP] A security exception occurred while trying to create an" +
          " instance of the custom factory class" +
          ": [" + trim(ex.getMessage()) +
          "]. Trying alternative implementations...");
      }
      // ignore
    }
  }
  // (3)测验从 classpath 目录下的 commons-logging.properties 文件中查找 org.apache.commons.logging.LogFactory 特点
  if (factory == null) {
    if (props != null) {
      if (isDiagnosticsEnabled()) {
        logDiagnostic(
          "[LOOKUP] Looking in properties file for entry with key '" + FACTORY_PROPERTY +
          "' to define the LogFactory subclass to use...");
      }
      String factoryClass = props.getProperty(FACTORY_PROPERTY);
      if (factoryClass != null) {
        if (isDiagnosticsEnabled()) {
          logDiagnostic(
            "[LOOKUP] Properties file specifies LogFactory subclass '" + factoryClass + "'");
        }
        factory = newFactory(factoryClass, baseClassLoader, contextClassLoader);
        // TODO: think about whether we need to handle exceptions from newFactory
      } else {
        if (isDiagnosticsEnabled()) {
          logDiagnostic("[LOOKUP] Properties file has no entry specifying LogFactory subclass.");
        }
      }
    } else {
      if (isDiagnosticsEnabled()) {
        logDiagnostic("[LOOKUP] No properties file available to determine" + " LogFactory subclass from..");
      }
    }
  }
  // (4)以上状况都不满意,实例化默许完结类 org.apache.commons.logging.impl.LogFactoryImpl
  if (factory == null) {
    if (isDiagnosticsEnabled()) {
      logDiagnostic(
        "[LOOKUP] Loading the default LogFactory implementation '" + FACTORY_DEFAULT +
        "' via the same classloader that loaded this LogFactory" +
        " class (ie not looking in the context classloader).");
    }
    factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
  }
  if (factory != null) {
    /**
     * Always cache using context class loader.
     */
    cacheFactory(contextClassLoader, factory);
    if (props != null) {
      Enumeration names = props.propertyNames();
      while (names.hasMoreElements()) {
        String name = (String) names.nextElement();
        String value = props.getProperty(name);
        factory.setAttribute(name, value);
      }
    }
  }
  return factory;
}

从 getFactory 办法的源码能够看出,其中心逻辑分为 4 步:

  • 首要,测验查找全局特点org.apache.commons.logging.LogFactory,假如指定了详细类,测验创立实例。

  • 使用 Java SPI 机制,测验在 classpatch 的 META-INF/services 目录下寻觅org.apache.commons.logging.LogFactory 的完结类。

  • 测验从 classpath 目录下的 commons-logging.properties 文件中查找org.apache.commons.logging.LogFactory 特点,假如指定了详细类,测验创立实例。

  • 以上状况假如都不满意,则实例化默许完结类,即org.apache.commons.logging.impl.LogFactoryImpl。

4.3 SPI 运用事例之 Spring Boot

Spring Boot 是依据 Spring 构建的结构,其规划目的在于简化 Spring 运用的装备、运转。在 Spring Boot 中,大量运用了主动安装来尽可能减少装备。

下面是一个 Spring Boot 进口示例,能够看到,代码十分简洁。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
    @GetMapping("/hello")
    public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
        return String.format("Hello %s!", name);
    }
}

那么,Spring Boot 是怎么做到寥寥几行代码,就能够运转一个 Spring Boot 运用的呢。咱们无妨带着疑问,从源码下手,一步步探究其原理。

4.3.1 @SpringBootApplication 注解

首要,Spring Boot 运用的发动类上都会标记一个

@SpringBootApplication 注解。

@SpringBootApplication 注解界说如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    // 略
}

除了 @Target、 @Retention、@Documented、@Inherited 这几个元注解,

@SpringBootApplication 注解的界说中还标记了 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 三个注解。

4.3.2 @SpringBootConfiguration 注解

从@SpringBootConfiguration 注解的界说来看,@SpringBootConfiguration 注解本质上便是一个 @Configuration 注解,这意味着被@SpringBootConfiguration 注解润饰的类会被 Spring Boot 识别为一个装备类。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(
        annotation = Configuration.class
    )
    boolean proxyBeanMethods() default true;
}

4.3.3 @EnableAutoConfiguration 注解

@EnableAutoConfiguration 注解界说如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

@EnableAutoConfiguration 注解包含了 @AutoConfigurationPackage与 @Import({AutoConfigurationImportSelector.class}) 两个注解。

4.3.4 @AutoConfigurationPackage 注解

@AutoConfigurationPackage 会将被润饰的类作为主装备类,该类所在的 package 会被视为根途径,Spring Boot 默许会主动扫描根途径下的一切 Spring Bean(被 @Component 以及承继 @Component 的各个注解所润饰的类)。——这便是为什么 Spring Boot 的发动类一般要置于根途径的原因。这个功用等同于在 Spring xml 装备中经过 context:component-scan 来指定扫描途径。@Import 注解的效果是向 Spring 容器中直接注入指定组件。@AutoConfigurationPackage 注解中注明晰@Import({Registrar.class})。Registrar 类用于保存 Spring Boot 的进口类、根途径等信息。

4.3.5 SpringFactoriesLoader.loadFactoryNames 办法

@Import(AutoConfigurationImportSelector.class) 表明直接注入AutoConfigurationImportSelector。

AutoConfigurationImportSelector 有一个中心办法getCandidateConfigurations 用于获取候选装备。该办法调用了SpringFactoriesLoader.loadFactoryNames 办法,这个办法即为 Spring Boot SPI 的要害,它担任加载一切 META-INF/spring.factories 文件,加载的进程由 SpringFactoriesLoader 担任。

Spring Boot 的 META-INF/spring.factories 文件本质上便是一个 properties 文件,数据内容便是一个个键值对。

SpringFactoriesLoader.loadFactoryNames 办法的要害源码:

// spring.factories 文件的格局为:key=value1,value2,value3
// 遍历一切 META-INF/spring.factories 文件
// 解析文件,获得 key=factoryClass 的类称号
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  String factoryTypeName = factoryType.getName();
  return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  // 测验获取缓存,假如缓存中有数据,直接回来
  MultiValueMap<String, String> result = cache.get(classLoader);
  if (result != null) {
    return result;
  }
  try {
    // 获取资源文件途径
    Enumeration<URL> urls = (classLoader != null ?
        classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
        ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    result = new LinkedMultiValueMap<>();
    // 遍历一切途径
    while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      UrlResource resource = new UrlResource(url);
      // 解析文件,得到对应的一组 Properties
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      // 遍历解分出的 properties,拼装数据
      for (Map.Entry<?, ?> entry : properties.entrySet()) {
        String factoryTypeName = ((String) entry.getKey()).trim();
        for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
          result.add(factoryTypeName, factoryImplementationName.trim());
        }
      }
    }
    cache.put(classLoader, result);
    return result;
  }
  catch (IOException ex) {
    throw new IllegalArgumentException("Unable to load factories from location [" +
        FACTORIES_RESOURCE_LOCATION + "]", ex);
  }
}

概括上面的办法,首要作了这些事:

加载一切 META-INF/spring.factories 文件,加载进程有 SpringFactoriesLoader 担任。

  • 在 CLASSPATH 中搜索一切 META-INF/spring.factories 装备文件。

  • 然后,解析 spring.factories 文件,获取指定主动安装类的全约束名。

4.3.6 Spring Boot 的 AutoConfiguration 类

Spring Boot 有各种 starter 包,能够依据实践项目需求,按需选材。在项目开发中,只要将 starter 包引进,咱们就能够用很少的装备,乃至什么都不装备,即可获取相关的才能。经过前面的 Spring Boot SPI 流程,只完结了主动安装作业的一半,剩下的作业怎么处理呢 ?

以 spring-boot-starter-web 的 jar 包为例,查看其 maven pom,能够看到,它依靠于 spring-boot-starter,一切 Spring Boot 官方 starter 包都会依靠于这个 jar 包。而 spring-boot-starter 又依靠于 spring-boot-autoconfigure,Spring Boot 的主动安装秘密,就在于这个 jar 包。

从 spring-boot-autoconfigure 包的结构来看,它有一个 META-INF/spring.factories ,明显使用了 Spring Boot SPI,来主动安装其中的装备类。

源码级深度理解 Java SPI

下图是 spring-boot-autoconfigure 的 META-INF/spring.factories 文件的部分内容,能够看到其中注册了一长串会被主动加载的 AutoConfiguration 类。

源码级深度理解 Java SPI

以 RedisAutoConfiguration 为例,这个装备类中,会依据 @ConditionalXXX 中的条件去决定是否实例化对应的 Bean,实例化 Bean 所依靠的重要参数则经过 RedisProperties 传入。

源码级深度理解 Java SPI

RedisProperties 中保护了 Redis 衔接所需求的要害特点,只要在 yml 或 properties 装备文件中,指定 spring.redis 最初的特点,都会被主动装载到 RedisProperties 实例中。

源码级深度理解 Java SPI

经过以上分析,现已一步步解读出 Spring Boot 主动装载的原理。

五、SPI 运用事例之 Dubbo

Dubbo 并未运用 Java SPI,而是自己封装了一套新的 SPI 机制。Dubbo SPI 所需的装备文件需放置在 META-INF/dubbo 途径下,装备内容办法如下:

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

与 Java SPI 完结类装备不同,Dubbo SPI 是经过键值对的办法进行装备,这样能够按需加载指定的完结类。Dubbo SPI 除了支撑按需加载接口完结类,还增加了 IOC 和 AOP 等特性。

5.1 ExtensionLoader 进口

Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,经过 ExtensionLoader,能够加载指定的完结类。

ExtensionLoader 的 getExtension 办法是其进口办法,其源码如下:

public T getExtension(String name) {
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    if ("true".equals(name)) {
        // 获取默许的拓宽完结类
        return getDefaultExtension();
    }
    // Holder,望文生义,用于持有方针方针
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    // 两层查看
    if (instance == null) {
        synchronized (holder) {
            instance = holder.get();
            if (instance == null) {
                // 创立拓宽实例
                instance = createExtension(name);
                // 设置实例到 holder 中
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

能够看出,这个办法的效果便是:首要查看缓存,缓存未射中则调用 createExtension 办法创立拓宽方针。那么,createExtension 是怎么创立拓宽方针的呢,其源码如下:

private T createExtension(String name) {
    // 从装备文件中加载一切的拓宽类,可得到“装备项称号”到“装备类”的映射联系表
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
        throw findException(name);
    }
    try {
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
            // 经过反射创立实例
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        // 向实例中注入依靠
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
            // 循环创立 Wrapper 实例
            for (Class<?> wrapperClass : wrapperClasses) {
                // 将当时 instance 作为参数传给 Wrapper 的结构办法,并经过反射创立 Wrapper 实例。
                // 然后向 Wrapper 实例中注入依靠,最后将 Wrapper 实例再次赋值给 instance 变量
                instance = injectExtension(
                    (T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
        throw new IllegalStateException("...");
    }
}

createExtension 办法的的作业进程能够概括为:

  1. 经过 getExtensionClasses 获取一切的拓宽类

  2. 经过反射创立拓宽方针

  3. 向拓宽方针中注入依靠

  4. 将拓宽方针包裹在相应的 Wrapper 方针中

以上进程中,第一个进程是加载拓宽类的要害,第三和第四个进程是 Dubbo IOC 与 AOP 的详细完结。

5.2 获取一切的拓宽类

Dubbo 在经过称号获取拓宽类之前,首要需求依据装备文件解分出拓宽项称号到拓宽类的映射联系表(Map<称号, 拓宽类>),之后再依据拓宽项称号从映射联系表中取出相应的拓宽类即可。相关进程的代码分析如下:

private Map<String, Class<?>> getExtensionClasses() {
    // 从缓存中获取已加载的拓宽类
    Map<String, Class<?>> classes = cachedClasses.get();
    // 两层查看
    if (classes == null) {
        synchronized (cachedClasses) {
            classes = cachedClasses.get();
            if (classes == null) {
                // 加载拓宽类
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}

这儿也是先查看缓存,若缓存未射中,则经过 synchronized 加锁。加锁后再次查看缓存,并判空。此刻假如 classes 仍为 null,则经过 loadExtensionClasses 加载拓宽类。下面分析 loadExtensionClasses 办法的逻辑。

private Map<String, Class<?>> loadExtensionClasses() {
    // 获取 SPI 注解,这儿的 type 变量是在调用 getExtensionLoader 办法时传入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
            // 对 SPI 注解内容进行切分
            String[] names = NAME_SEPARATOR.split(value);
            // 检测 SPI 注解内容是否合法,不合法则抛出反常
            if (names.length > 1) {
                throw new IllegalStateException("more than 1 default extension name on extension...");
            }
            // 设置默许称号,参阅 getDefaultExtension 办法
            if (names.length == 1) {
                cachedDefaultName = names[0];
            }
        }
    }
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    // 加载指定文件夹下的装备文件
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

loadExtensionClasses 办法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 办法加载指定文件夹装备文件。SPI 注解解析进程比较简单,无需多说。下面咱们来看一下 loadDirectory 做了哪些事情。

private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
    // fileName = 文件夹途径 + type 全约束名
    String fileName = dir + type.getName();
    try {
        Enumeration<java.net.URL> urls;
        ClassLoader classLoader = findClassLoader();
        // 依据文件名加载一切的同名文件
        if (classLoader != null) {
            urls = classLoader.getResources(fileName);
        } else {
            urls = ClassLoader.getSystemResources(fileName);
        }
        if (urls != null) {
            while (urls.hasMoreElements()) {
                java.net.URL resourceURL = urls.nextElement();
                // 加载资源
                loadResource(extensionClasses, classLoader, resourceURL);
            }
        }
    } catch (Throwable t) {
        logger.error("...");
    }
}

loadDirectory 办法先经过 classLoader 获取一切资源链接,然后再经过 loadResource 办法加载资源。咱们持续跟下去,看一下 loadResource 办法的完结。

private void loadResource(Map<String, Class<?>> extensionClasses,
  ClassLoader classLoader, java.net.URL resourceURL) {
    try {
        BufferedReader reader = new BufferedReader(
            new InputStreamReader(resourceURL.openStream(), "utf-8"));
        try {
            String line;
            // 按行读取装备内容
            while ((line = reader.readLine()) != null) {
                // 定位 # 字符
                final int ci = line.indexOf('#');
                if (ci >= 0) {
                    // 截取 # 之前的字符串,# 之后的内容为注释,需求忽略
                    line = line.substring(0, ci);
                }
                line = line.trim();
                if (line.length() > 0) {
                    try {
                        String name = null;
                        int i = line.indexOf('=');
                        if (i > 0) {
                            // 以等于号 = 为界,截取键与值
                            name = line.substring(0, i).trim();
                            line = line.substring(i + 1).trim();
                        }
                        if (line.length() > 0) {
                            // 加载类,并经过 loadClass 办法对类进行缓存
                            loadClass(extensionClasses, resourceURL,
                                      Class.forName(line, true, classLoader), name);
                        }
                    } catch (Throwable t) {
                        IllegalStateException e = new IllegalStateException("Failed to load extension class...");
                    }
                }
            }
        } finally {
            reader.close();
        }
    } catch (Throwable t) {
        logger.error("Exception when load extension class...");
    }
}

loadResource 办法用于读取和解析装备文件,并经过反射加载类,最后调用 loadClass 办法进行其他操作。loadClass 办法用于首要用于操作缓存,该办法的逻辑如下:

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL,
    Class<?> clazz, String name) throws NoSuchMethodException {
    if (!type.isAssignableFrom(clazz)) {
        throw new IllegalStateException("...");
    }
    // 检测方针类上是否有 Adaptive 注解
    if (clazz.isAnnotationPresent(Adaptive.class)) {
        if (cachedAdaptiveClass == null) {
            // 设置 cachedAdaptiveClass缓存
            cachedAdaptiveClass = clazz;
        } else if (!cachedAdaptiveClass.equals(clazz)) {
            throw new IllegalStateException("...");
        }
    // 检测 clazz 是否是 Wrapper 类型
    } else if (isWrapperClass(clazz)) {
        Set<Class<?>> wrappers = cachedWrapperClasses;
        if (wrappers == null) {
            cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
            wrappers = cachedWrapperClasses;
        }
        // 存储 clazz 到 cachedWrapperClasses 缓存中
        wrappers.add(clazz);
    // 程序进入此分支,表明 clazz 是一个一般的拓宽类
    } else {
        // 检测 clazz 是否有默许的结构办法,假如没有,则抛出反常
        clazz.getConstructor();
        if (name == null || name.length() == 0) {
            // 假如 name 为空,则测验从 Extension 注解中获取 name,或运用小写的类名作为 name
            name = findAnnotationName(clazz);
            if (name.length() == 0) {
                throw new IllegalStateException("...");
            }
        }
        // 切分 name
        String[] names = NAME_SEPARATOR.split(name);
        if (names != null && names.length > 0) {
            Activate activate = clazz.getAnnotation(Activate.class);
            if (activate != null) {
                // 假如类上有 Activate 注解,则运用 names 数组的第一个元素作为键,
                // 存储 name 到 Activate 注解方针的映射联系
                cachedActivates.put(names[0], activate);
            }
            for (String n : names) {
                if (!cachedNames.containsKey(clazz)) {
                    // 存储 Class 到称号的映射联系
                    cachedNames.put(clazz, n);
                }
                Class<?> c = extensionClasses.get(n);
                if (c == null) {
                    // 存储称号到 Class 的映射联系
                    extensionClasses.put(n, clazz);
                } else if (c != clazz) {
                    throw new IllegalStateException("...");
                }
            }
        }
    }
}

如上,loadClass 办法操作了不同的缓存,比如 cachedAdaptiveClass、cachedWrapperClasses 和 cachedNames 等等。除此之外,该办法没有其他什么逻辑了。

参阅资料

* Java SPI 思想梳理

  • Dubbo SPI
  • springboot 中 SPI 机制
  • SpringBoot 的主动安装原理、自界说 starter 与 spi 机制,一扫而光