JAVA SPI 机制
前言
一、SPI是什么?
SPI全称Service Provider Interface( 服务提供商接口)。
在面向对象的设计中,根据依赖倒转原则,模块间应该基于接口编程,而不是对实现类进行硬编码。一旦在代码中设计到具体的实现类,那么当我们需要替换另一种实现时,就需要修改代码。为了实现在模块装配的时候,不在模块里写死代码,我们需要一种服务发现机制,而java spi就是提供了这样一种机制。这种机制就类似于IoC的思想,将代码装配的控制权移交到了代码之外。
依赖倒转原则:
1)高层模块不应该依赖底层模块,二者都应该依赖其抽象
2)抽象不应该依赖细节,细节应该依赖抽象
3)依赖倒转的中心思想是面向接口编程
二、JAVA SPI的具体约定
当使用JAVA SPI时,需要在src/main/resources文件夹下创建目录META-INF/services/,并在这个目录下创建指定接口的全限定性类名文件,该文件的内容就是该接口的具体实现类的全限定性类名。
当服务消费者需要调用这个接口的实现类时,就能通过 META-INF/services/ 里的配置文件得到具体的实现类名,并加载实例化,完成代码的装配。
三、具体案例
1.创建接口
代码如下(示例):
package com.wgc.spi;
public interface Animal {
/**
* 输出动物叫声
*/
void say();
}
2.创建实现类
共创建了两个实现类,Cat和Dog
代码如下(示例):
package com.wgc.spi.impl;
import com.wgc.spi.Animal;
public class Cat implements Animal {
@Override
public void say() {
System.out.println("喵喵");
}
}
package com.wgc.spi.impl;
import com.wgc.spi.Animal;
public class Dog implements Animal {
@Override
public void say() {
System.out.println("汪汪");
}
}
3、在META-INF/services文件夹下创建接口同名文件

配置文件com.wgc.spi.Animal的内容是:
com.wgc.spi.impl.Cat
com.wgc.spi.impl.Dog
注意:如果有多个实现类需要换行表示。
4、在类中使用
public class SpiMainClass {
public static void main(String[] args) {
ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
for (Animal animal:load) {
animal.say();
}
}
}
输出结果为:
喵喵
汪汪
因为我们在配置文件中配置了两个实现类Cat、Dog,所以ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);加载了两个实现类,也就在循环中输出了“喵喵”、“汪汪”。
四、原理分析
我们debug一下ServiceLoader.load(Animal.class);
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这里面又执行了一些简单的代码,直到运行到reload()方法,实例化了LazyIterator对象lookupIterator = new LazyIterator(service, loader);
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
LazyIterator是ServiceLoader的内部类,当我们迭代遍历ServiceLoader对象时,就会执行LazyIterator类的nextService()和hasNextService()方法,代码如下:
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// SPI查询配置文件路径
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 加载SPI配置文件
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 将SPI配置文件的内容加载到ArrayList后,返回list的Iterator对象
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 {
// 生成SPI实现类的class对象
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 {
// 实例化一个实现类对象,并将其存放到ServiceLoader的成员变量providers中
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
}
......
}
五、实际案例
JDBC(Java Database Connectivity)为访问不同的数据库提供了一种统一的途径,使程序员使用JDBC可以连接任何提供了JDBC驱动程序的数据库系统。而jdbc加载mysql数据库驱动的就用到了SPI技术。
我们使用jdbc创建一个连接的方式是:
DriverManager.getConnection(conf.getUrl(), conf.getUser(), conf.getPwd());
而加载DriverManager类时,会先执行下面的静态代码块:
public class DriverManager {
// 将jdbc驱动类注册到这个list
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
......
private DriverManager(){}
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在loadInitialDrivers();方法中就是通过ServiceLoader加载的mysql的jar包中META-INF/services文件夹下的java.sql.Driver文件中配置的具体驱动,代码如下:
/**
* 注意:我已将源程序中的英文注释删除,请自行查阅
*/
private static void loadInitialDrivers() {
String drivers;
try {
// 查看系统属性中是否存在名为jdbc.drivers的属性,根据debug接口,返回为null
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// 下面的代码通过SPI加载了驱动程序
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通过ServiceLoader加载驱动类,mysql8的驱动包中Driver的实现类是com.mysql.cj.jdbc.Driver
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
// 此时会加载com.mysql.cj.jdbc.Driver
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 如果在系统属性中jdbc.drivers的配置,那么就在classpath下加载对应的类
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);
}
}
}
在加载com.mysql.cj.jdbc.Driver时,会将Driver对象注册registeredDrivers 集合中,代码如下:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// 将驱动类对象加载到 DriverManager
static {
try {
// 这里将com.mysql.cj.jdbc.Driver的对象存放在了DriverManager的registeredDrivers 对象中
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
本文深入探讨Java SPI(服务提供者接口)机制,讲解其如何避免硬编码实现类,实现模块间的灵活装配。通过示例展示SPI的具体使用过程,包括接口定义、实现类创建、配置文件设置及代码调用。同时,分析SPI的内部原理,以及在实际场景如JDBC中的应用。
1万+

被折叠的 条评论
为什么被折叠?



