SPI——Service Provider Interface
作者:互联网
从一个示例开始
下面是一个用于打印的接口,它会将message
打印到控制台,但以什么格式打印是实现类规定的。
package top.yudoge.springserials.basic.spi;
public interface Printer {
void print(String message);
}
LinePrinter
简单的调用System.out.println
,让每次打印在一个新的行中:
package top.yudoge.springserials.basic.spi.impl;
import top.yudoge.springserials.basic.spi.Printer;
public class LinePrinter implements Printer {
@Override
public void print(String message) {
System.out.println(message);
}
}
SquarePrinter
将要打印的message
用方块包裹起来:
package top.yudoge.springserials.basic.spi.impl;
import top.yudoge.springserials.basic.spi.Printer;
public class SquarePrinter implements Printer {
@Override
public void print(String message) {
// print top edge
printNTimes("#", message.length() + 2);
// print side and message
System.out.println("|" + message + "|");
// print bottom edge
printNTimes("#", message.length() + 2);
}
public void printNTimes(String string, int times) {
for (int i=0; i<times; i++)
System.out.print(string);
System.out.println();
}
}
Java提供的ServiceLoader
类会扫描类路径下的META-INF/services/
中的文件,文件名是接口名,文件中的每一行是一个该接口的实现类:
下面是resources/META-INF/services/top.yudoge.springserials.basic.spi.Printer
文件中的内容:
top.yudoge.springserials.basic.spi.impl.SquarePrinter
这里我们只写了SquarePrinter
,下面看看如何使用ServiceLoader
构建这个实现类:
public class Main {
public static void main(String[] args) {
ServiceLoader<Printer> printers = ServiceLoader.load(Printer.class);
for (Printer printer : printers) {
printer.print("hello^_^");
}
}
}
结果:
修改META-INF/service
下的文件:
top.yudoge.springserials.basic.spi.impl.LinePrinter
top.yudoge.springserials.basic.spi.impl.SquarePrinter
重新运行,ServiceLoader
加载了两个实现类:
有什么用?
这就是SPI?这破玩意儿的实际用处在哪?
在这个例子中,我们确实看不到实际用处,但是,让我们来通过分析JDBC程序来看看它的作用。
通过JDBC来访问数据库通常需要这么两步:
//1、注册驱动
Class.forName("com.mysql.cj.jdbc.Driver");
//2、通过DriverManager获取数据库连接对象
Connection con = DriverManager.getConnection("jdbc:mysql://localhost:3306/sys", "root", "密码");
先解释一下和SPI无关的,为什么Class.forName
能注册驱动
进入到MySQL的Driver实现中,可以看到这样的代码:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
static {
DriverManager.registerDriver(new Driver());
}
}
所以,是该类被加载之后触发了里面的静态代码块,静态代码块调用JDK中的DriverManager
完成了驱动的注册
不手动注册驱动行吗?
下面我们写这样的代码,我们没在任何位置手动注册了MySQL驱动:
public class Main {
public static void main(String[] args) throws SQLException {
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/dbconcept", "root", "root");
System.out.println(connection);
}
}
但是结果表明,连接还是建立成功了:
是ServiceLoader在工作
我们查看DriverManager
的代码,发现如下静态代码块:
/**
* 通过检查系统属性——`jdbc.properties`和使用ServiceLoader机制
* 加载默认JDBC驱动
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
然后loadInitialDrivers
方法里面有这样一段:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
这不就是对每一个classpath
下的META-INF/services
下的文件名为java.sql.Driver
的文件中的所有Driver实现类的全限定名进行类加载吗?
而MySQL的类路径下确实有这个文件:
里面的内容就是这个驱动
所以,流程是:
- DriverManager类被你使用,所以它的类初始化代码被执行
- 它的类初始化代码中使用ServiceLoader查找类路径下
META-INF/server/java.sql.Driver
中描述的Driver实现类 - 对使用迭代器迭代每一个Driver实现类,这样,实现类就会被加载
- MySQL实现类的类初始化代码中调用了
DriverManager.registerDriver
可是!到底有什么用?
考虑上面的数据库驱动的场景,这个场景中有三个角色:
API定义者
:Java定义了Driver的APIAPI实现者
:数据库厂商实现Driver,如MySQLAPI使用者
:我们
在这里,API定义者对API使用者使用何种实现一无所知,它可能有无数种实现,并且该数目可能会随时间不断扩张。
在以往的开发中,API定义者和API实现者往往是一起的,比如最初的Printer
例子:
在这种场景下,API定义者对系统中可能存在的实现很清楚,API调用者通常会直接使用定义者提供的实现类,而不是自己指定一个新的实现类。
而对于JDBC的例子,API调用者必须提供一个API实现者提供的实现类,因为在这种场景下,API定义者、API实现者是分离的。
而SPI机制规定了API实现者如何向系统中递交自己的实现类,该实现类的发现是自动的(一般是由某种框架扫描发现,比如DriverManager),由于实现类会被自动发现,所以API调用者的代码只需使用API定义者提供的接口,而无需显式的编写、创建API实现者的实现类。这让它可以极其方便的替换实现。
对于上面一段话,可以想象,使用JDBC时,如果想从MySQL切换到Oracle,是不是直接替换驱动、修改数据库地址即可,因为你的代码中从来没有显式的依赖任何MySQL驱动中的类,只是在用JDBC规范中的接口在工作。
再想想,如果你的类中依赖的不是
java.sql.Driver
接口,而是遍地都是com.mysql.cj.jdbc.Driver
这个实现类,那么你换到Oracle的时候可能就有得忙活了。
使用SPI机制加载的类,到底何时、如何被加载?
下面我们通过分析代码来分析标题的问题。
上面,Java的DriverManager
中调用了ServiceLoader.load(java.sql.Driver.class)
后,得到了一个ServiceLoader
对象,这个对象是个可迭代对象,随后DriverManager
又对它进行了迭代。
从这个迭代过程中啥也没干,就大概可以说明ServiceLoader.load
中并未实际的加载那些实现类,而那些实现类在被迭代时加载,否则为什么要进行这个迭代呢?
ServiceLoader.load干了什么?
下面是ServiceLoader.load(Class)
方法的代码,它获取了调用者线程的线程上下文类加载器,并且调用了重载方法,把这个线程上下文类加载器传了进去:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
下面是重载的load
方法,它只是简单的创建了ServiceLoad
对象并返回:
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
目前都未发生任何对实现类的加载,关于线程上下文类加载器,我也不知道是个啥,再点到ServiceLoader
的构造方法中看看:
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
只是做了检查,如果classLoader
是null的话使用系统类加载器,也就是AppClassLoader
,但注意,这里cl
并不为null,是调用者线程的线程上下文类加载器,稍后我可能会查什么是线程类上下文加载器。
被构造器调用的reload
方法看起来也很简单,它只是初始化了一个什么迭代器对象。
如上是通过调用ServiceLoader
的静态方法load
来返回一个ServiceLoader
实例的整个调用链,这个调用链中没有对实现类进行加载的代码。所以,对那些Driver实现的加载,必定在稍后的遍历过程中。
我们简单看下ServiceLoader.iterator
方法返回的迭代器:
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
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();
}
};
}
这里引入了一个什么knownProviders
,迭代器先对这个进行迭代,之后再对我们在构造方法中看到的lookupIterator
进行迭代。可能有点令人迷惑,不过注意,knownProviders
是基于providers
这个东西创建的,这个东西已经在构造方法中被我们清空了,所以在上面DriverManager
的调用链中,我们可以完全忽视迭代过程中knownProviders
所起的作用,所以代码被简化成了这样:
public Iterator<S> iterator() {
return new Iterator<S>() {
public boolean hasNext() {
return lookupIterator.hasNext();
}
public S next() {
return lookupIterator.next();
}
};
}
那么,我们就该看lookupIterator
了,因为这明显是对它的一次委托,它是一个LazyIterator
,看起来是个什么懒加载的迭代器,我们简化了它的hasNext
的代码:
public boolean hasNext() {
return hasNextService();
}
进入hasNextService
:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// === core start ===
// PREFIX="META-INF/services/"
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
// === core end ===
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
// === core start ===
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
// === core end ===
}
nextName = pending.next();
return true;
}
上面,只需要看我的注释中间的部分,总的来说,就是去META-INF/service下找指定文件(根据传入ServiceLoader的类名),然后用classloader去加载这个文件,这个classloader在我们的调用链中就是调用者线程上下文ClassLoader。
得到文件之后,调用parse
对每一行进行一个解析,总之,LazyIterator的hasNext的作用就是判断那个文件中是否还有行,如果有,把这一行解析出来,设置给nextName
,供稍后迭代器的next
方法使用。而pending
就是hasNext
和next
中交换状态的一个变量,有了它,hasNext
不用在连续调用hasNext
时重复解析同一行。
上面的这一段都是我猜的,我没力气分析这些代码,所以咱们看个大概,这些算法的细节在我们理解源码的过程中并没什么作用。
LazyIterator的next
方法调用了nextService
方法,直接看:
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
// 核心代码,初始化类,使用了loader
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 subtype");
}
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
}
所以,实际的类加载发生在迭代过程中的next
时,在这个时候,加载这个类,创建该类的一个实例,并且:
- 加载类时使用了外部传入的classLoader,外部没传入的时候就是那个线程上下文类加载器
- 想使用SPI机制的类必须有无参构造,因为这里的
newInstance
没传入参数
线程上下文类加载器到底是什么?
下面内容来自知乎文章java ContextClassLoader (线程上下文类加载器)
类
java.lang.Thread
中的方法getContextClassLoader()
和setContextClassLoader(ClassLoader cl)
用来获取和设置线程的上下文类加载器。如果没有通过
setContextClassLoader(ClassLoader cl)
方法进行设置的话,Thread默认继承父线程的Context ClassLoader
(注意,是父线程不是父类)。如果你整个应用中都没有对此作任何处理,那么所有的线程都会以System ClassLoader作为线程的上下文类加载器。
所以,我们可以确定,在我们没设置线程上下文类加载器的时候,使用的是AppClassLoader
。
下面的代码,首先SPI机制隐式的加载了MySQL Driver实现类,然后我们获得了它,并打印了它的类加载器,又打印了当前线程的类加载器:
public class Main {
public static void main(String[] args) throws SQLException {
Driver driver = DriverManager.getDriver("jdbc:mysql://localhost:3306/dbconcept");
System.out.println(driver.getClass().getClassLoader());
System.out.println(Thread.currentThread().getContextClassLoader());
}
}
由于隐式加载Driver类的也是这个Main线程,所以它俩是一个线程,那么上下文加载器也一样,所以结果都是AppClassLoader
:
绕这么一圈干嘛?
想想被DriverManager
直接加载的类会是由什么类加载器加载?
DriverManager
是Java核心组件,它是由BootstrapClassLoader
加载的,那么它所直接加载的类也都会由BootstrapClassLoader
进行加载。但是,BootstrapClassLoader
只能加载%JAVA_HOME%/lib
下的类,它自然无法加载驱动实现厂商给的类,而且,它也没有任何办法拿到底层的AppClassLoader
,这是双亲委派模型的单向限制。
所以,所以!相当于调用者线程的线程类加载器将AppClassLoader
传递进来,然后再用它来加载驱动厂商的实现。
所以,线程上下文加载器就是用来解决Java中的核心库反向加载其它由Java用户提供的类时,让它把底层ClassLoader传递进来的一个不太优雅的解决办法。
标签:Service,Driver,ServiceLoader,SPI,API,线程,Interface,public,加载 来源: https://www.cnblogs.com/lilpig/p/16467010.html