JAVA SPI

spi 使用

首先,通过一张图来看,完成 spi 的实现,需要哪些操作,需要遵循哪些规范?

1.代码编写

既然是 spi,那么就必须先定义好接口。其次,就是定义好接口的实现类。

2.创建一个文件夹

在项目的\src\main\resources\下创建\META-INF
\services 目录(笔者在网上找了很多文章,很多都没有告知具体这个文件夹放在哪,放在其他位置下无法加载得到)

3.文件夹下增加配置文件

在上面 META-INF
\services 的目录下再增加一个配置文件,这个文件必须以接口的全限定类名保持一致,例如:com.jiaboyan.test.HelloService

4.配置文件增加描述

上面介绍 spi 时说道,除了代码上的接口实现之外,你还需要把该实现的描述提供给 JDK。那么,此步骤就是在配置文件中撰写接口实现描述。很简单,就是在配置文件中写入具体实现类的全限定类名,如有多个便换行写入。

5.使用 JDK 来载入

编写 main()方法,输出测试接口。使用 JDK 提供的 ServiceLoader.load()来加载配置文件中的描述信息,完成类加载操作。

接口定义:

1
2
3
4
public interface HelloService {

void hello();
}

接口实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HelloService1Impl implements HelloService {

@Override
public void hello() {
System.out.println("hello jiaboyan");
}
}

public class HelloService2Impl implements HelloService {
@Override
public void hello() {
System.out.println("hello world");
}
}

添加 JDK 描述,在 META-INF\services 目录下:

1
2
com.jiaboyan.test.impl.HelloService1Impl
com.jiaboyan.test.impl.HelloService2Impl

编写 main()方法:

1
2
3
4
5
6
7
8
9
public class Test {

public static void main(String[] agrs) {
ServiceLoader<HelloService> loaders = ServiceLoader.load(HelloService.class);
for (HelloService helloService : loaders) {
helloService.hello();
}
}
}

SPI-机制

SPI 全称为 Service Provider Interface,是 JDK 内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现机制。例如:有个接口想在运行时才发现具体的实现类,那么你只需要在程序运行前添加一个实现即可,并把新加的实现描述给 JDK 即可。此外,在程序的运行过程中,也可以随时对该描述进行修改,完成具体实现的替换。

Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC、JCE、JNDI、JAXP 和 JBI 等。

这些 SPI 的接口是由 Java 核心库来提供,而 SPI 的实现则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)中。例如:JDBC 的实现 mysql 就是通过 maven 被依赖进来。

那么问题来了,SPI 的接口是 Java 核心库的一部分,是由引导类加载器(Bootstrap Classloader)来加载的。SPI 的实现类是由系统类加载器(System ClassLoader)来加载的。

引导类加载器在加载时是无法找到 SPI 的实现类的,因为双亲委派模型中规定,引导类加载器 BootstrapClassloader 无法委派系统类加载器 AppClassLoader 来加载。这时候,该如何解决此问题?

线程上下文类加载由此诞生,它的出现也破坏了类加载器的双亲委派模型,使得程序可以进行逆向类加载

线程上下文类加载器

通过名字可知,线程上下文类加载,就是当前线程所拥有的类加载器,可通过 Thread.currentThread()获取当前线程。

线程上下文类加载器(Thread Context ClassLoader)可以通过 java.lang.Thread 类的 setContextClassLoader()方法设置,创建线程时候未指定的话,则默认从父线程中继承。

那父线程中也没指定呢?那么会默认为应用程序的类加载器。例如:main 方法的线程上下文类加载器就是 sun.misc.Launcher$AppClassLoader。

前两篇文章中,我们讲解了类加载器的双亲委派模型,该模型的实现是通过类加载器中的 parent 属性(父加载器)来完成的,默认统一交给最上层类加载器去尝试加载。

那,这个线程上下文类加载器又是干啥的?

在介绍线程上下文类加载前,我们先了解下 Java 的 SPI 机制。

线程上下文类加载实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class JVMTest6 {

public static void main(String[] agrs) throws ClassNotFoundException {
ClassLoader loader = JVMTest6.class.getClassLoader();
System.out.println(loader); //默认是应用类加载器

//此时获得上下文类加载器:
ClassLoader loader2 = Thread.currentThread().getContextClassLoader();
System.out.println(loader2);//默认也是应用类加载器

//设置为自定义类加载器:
Thread.currentThread().setContextClassLoader(
new ClassLoaderTest("d:/"));
System.out.println(Thread.currentThread().getContextClassLoader());

//使用自定义类加载器加载:
Class c = Thread.currentThread().getContextClassLoader().loadClass("HelloWorld");
System.out.println(c.getClassLoader());//线程上下文类加载器

ClassLoader loader3 = String.class.getClassLoader();
System.out.println(loader3);//启动类加载器 = null
}
}

测试结果

1
2
3
4
5
sun.misc.Launcher$AppClassLoader@41dee0d7
sun.misc.Launcher$AppClassLoader@41dee0d7
ClassLoaderTest@516a4aef
ClassLoaderTest@516a4aef
null

spi 具体实现:

在下面代码中,通过 SPI 方式来完成 java.sql.Driver 接口实现类的类加载操作。

java.sql.DriverManager 包中:

1
2
3
4
5
6
7
8
9
10
11
12
13
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//通过SPI方式,读取META-INF/services下文件中的类名:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {}
return null;
}
});

获取到 ServiceLoader 对象后,进行遍历操作,遍历出所有 META-INF/services 文件夹下的实现类名称,之后再进行 Class.forName(“”)类加载操作。类加载操作在 driversIterator.next()中完成。

java.util.ServiceLoader 包中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static <S> ServiceLoader<S> load(Class<S> service) {
//获取线程上下文类加载器:
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//生成ServiceLoader对象:
return ServiceLoader.load(service, cl);
}

public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader){
return new ServiceLoader<>(service, loader);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = svc;
loader = cl;
reload();
}

在获取 ServiceLoader 对象时,获取了此时线程上下文中的类加载器,将此类加载赋值给 ServiceLoader 类中的 loader 成员变量。在后续类加载过程中,都是使用的此类加载来完成。这一步的操作,直接打破了双亲委派模型,实现了逆向类加载。

1
2
3
4
5
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {}

通过 debug 发现,driversIterator.next()方法内部会调用 Class c = Class.forName(cn, false, loader)方法进行类加载操作。而此时传递的 loader 就是之前获取的线程上下文类加载器,传递的 cn 就是 META-INF/services 文件中的具体实现类。

由于笔者是通过本地的 test 进行测试,所以上文中涉及到的类加载器都是 AppClassLoader 系统类加载器。

作者:贾博岩
链接:https://www.jianshu.com/p/e4262536000d
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。