Java Service Provider Interface 和类加载机制有些关系,接着 一次类加载机制实践 这个文章刚好也记录一下。

简介

SPI 在 Wiki 上 就用了一句话就基本说清楚了它是干什么的。作用就是定义一些基础接口让第三方库去实现,之后依赖这些基础接口的程序能在发布之后甚至运行期间,通过引入不同的第三方库从而让服务表现出不同的行为。更简单的说就是实现通常说的 “插件” 的一种方式。Oracle 有专门介绍这个东西的文档,并举了 Audio Service 作为例子,在这里:Introduction to the Service Provider Interfaces

还有 Java Service Provider Interface | Baeldung 这篇文章也很基础,会带着你先设计一个接口作为 Service,再为 Service 提供具体的 Service Provider 实现,最终通过 ServiceLoader 将 Service 和 Service Provider 联系起来,完整的体会一下 SPI 的功能。还提供了引申的链接,去看看 Java 提供了哪些 SPI ,很值得看看。

我这里主要想记录的是 SPI 的类加载机制。

SPI 的加载

从上述 SPI 的相关内容中我们能看到 SPI 和我 一次类加载机制实践 这里说的 Hook 很像。同样是提供一个接口或 Service 让第三方来实现,之后在程序运行过程中,主动到某个固定路径下做搜索,找到符合条件的实现类加载入内存,再去调用加载进来的类的实现 method,从而动态的改变服务的行为。

借用 SPI 的概念,接口叫做 Service,实现接口提供具体行为的叫做 Service Provider,还需要有个角色去使用 Service,姑且称为 Service User。除此之外最重要的还需要一个东西作为 “粘合剂” 将 Service 和 Service Provider “粘合” 起来交给 Service User 使用,可以把它理解为 Factory 模式中的 Factory。这样对于 Service User 来说它只需要认识 Service 而不用关心具体的 Service Provider 是什么。Service User 在编译和类加载过程中都只需要编译、加载 Service 即可。之后由 “粘合剂” 去负责通过 java.util.ServiceLoader 找到具体的 Service Provider 把他们加载出来交给 Service User 用。

Java 推出 SPI 后自己定义了很多 Service,并且实现了 ”粘合剂“ 去加载 Service Provider,都藏在 rt.jar 内,比如大名鼎鼎的 java.sql.Driver Service,会被 java.sql.DriverManager 使用,并在 DriverManager 内的 loadInitialDrivers method 加载 java.sql.Driver 的 Service Provider :

static {
  loadInitialDrivers()
}

private static void loadInitialDrivers() {
  ...
  ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
  Iterator<Driver> driversIterator = loadedDrivers.iterator();
  ...
  while(driversIterator.hasNext()) {
    driversIterator.next();
  }
  ...
}

在 rt.jar 内的DriverManager 要由 Bootstrap 加载器加载,也由该加载器去调用 static 完成类初始化。但初始化过程中,DriverManager 又要通过 ServiceLoader 找到第三方实现的 Service Provider 类并将之加载,加载过程不能使用 Bootstrap 加载器完成,所以只能用某个方法将 Bootstrap 的某个子加载器传递到 DriverManager 内,并用子加载器去加载第三方的 Service Provider。但此时是 DriverManager 初始化过程,通常的传递参数的办法是无法使用的,所以 Java 在 Thread 内埋藏了一个 Context Class Loader 来完成这个事情,是不是感觉有点 Hack,打补丁的感觉。。。The Dreaded Thread Context Class Loader - Paremus 这篇文章写了 Thread Context Class Loader 的来历以及它带来的问题。一般来说一个能被藏起来传的到处都是的东西都比较让人担心,看上去这个 Context Class Loader 也会带来不少问题。不过先不管这些,我们来看看这个 Context Class Loader 从设置到被 ServiceLoader 使用的过程。

Context Class Loader

很惭愧我实在是找不到或者说理不清第一个 Thread 是怎么启动的。。。我只能先写一些我所知道的。

首先,如果系统还未初始化过 System Class Loader 也即 AppClassLoader,那第一个想要获取 System Class Loader 的线程会默认的设置 Context Class Loader 为 System Class Loader。

先看 java.lang.ClassLoader:

// 一般用来获取 System Class Loader 的方法就是 getSystemClassLoader
public static ClassLoader getSystemClassLoader() {
    // 获取 System Class Loader 时候会先初始化这个 Class Loader
    initSystemClassLoader();
    ...
}
private static synchronized void initSystemClassLoader() {
  ...
  // 细节不写了,总之就是这里面在判断出来 System Class Loader 没有初始化后会开始初始化 System Class Loader
  // 初始化完了以后会把 System Class Loader 写入当前执行到这个地方的 Thread 的 Context Thread Loader 里
  // 如果已经初始化过了 System Class Loader 则不会再初始化一遍也不会设置 Thread 的 Context Thread Loader 
}

以上有可能是第一个获取到 Thread Context Class Loader 的 Thread 的获取方法。

之后只要是 New Thread 都会去继承 parent 的 Thread Context Class Loader,在 java.lang.Thread 内:

private void init(...) {
  ...
  if (security == null || isCCLOverridden(parent.getClass()))
    this.contextClassLoader = parent.getContextClassLoader();
  else
    this.contextClassLoader = p
  ...

所以有个事情至少基本能确认,就是没有主动设置的话,Thread Context Class Loader 默认就会是 System Class Loader,即应用加载器。

回到 java.lang.ServiceLoaderload 内:

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

看到在不明确传递 Class Loader 时,load 方法会读取当前线程的 Thread Context Class Loader 去加载第三方的 Service Provider。从而实现在 Bootstrap 加载器加载类似 java.lang.DriverManager 类时能用应用加载器去加载第三方 Service Provider。

能不能用 SPI 去实现之前文章中的 Hook 机制?

一次类加载机制实践 这个文章中说的 Hook 需求有两个,一个是定义一个 Service,主程序使用 Service 并能动态的从某个固定路径下通过搜索 Jar 包的方式找到 Service Provider;另一个是加载的 Service Provider 要做到依赖隔离,主程序已经加载过的某个 Class 在加载 Service Provider 时候还要再加载一遍。

SPI 能实现第一个需求,即发现 Service Provider 并进行加载的需求,但 SPI 本身并没有提供隔离机制,实现依赖隔离是加载器的事情,跟 SPI 没有关系。

所以对这个问题的回答是,能用 SPI 去实现,只是还需要配合那个文章中说的内容去实现能做到依赖隔离的加载器,通过 SPI 去固定路径动态发现实现某接口的类,用自定义加载器加载目标类实现依赖隔离。两个东西不冲突。