关于java:源码级深度理解-Java-SPI

38次阅读

共计 26699 个字符,预计需要花费 67 分钟才能阅读完成。

作者: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<T> 接口,所以,它是能够迭代遍历的。实际上,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 实现。如下图所示:

  • 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,来主动拆卸其中的配置类。

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

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

RedisProperties 中保护了 Redis 连贯所须要的要害属性,只有在 yml 或 properties 配置文件中,指定 spring.redis 结尾的属性,都会被主动装载到 RedisProperties 实例中。

通过以上剖析,曾经一步步解读出 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 机制,一网打尽

正文完
 0