概念
Java SPI(Service Provider Interface)是 Java 平台中的一种机制,用于实现模块化、可插拔的架构。它允许开发人员定义服务接口,并通过特定的配置文件,让不同的实现者提供其实现。这种方式使得应用程序能够在运行时动态地发现和加载服务实现,而无需在编译时确定具体的实现类。
Java里面很多场景都用到了该机制,典型的如JDBC,Java提供了标准的驱动接口(java.sql.Driver),不同的数据库厂商可以提供对应的实现,比如MySQL,Oracle,SQLServer等。
代码示例
概念很简单,下面我们动手写个简单的代码示例,来看下SPI是如何运作的。
我们首先定义一个日志服务接口,很简单就一个log方法:
/**
* Log Service
*/
public interface Log {
void log(String message);
}
接下来我们提供两个不同的实现类:
/**
* 文件日志:将日志写入到文件
*/
public class FileLog implements Log {
@Override
public void log(String message) {
System.out.println("FileLog: " + message);
}
}
/**
* ELK日志:将日志写入ELK日志中心
*/
public class ELKLog implements Log{
@Override
public void log(String message) {
System.out.println("ELKLog: " + message);
}
}
现在服务有了,服务实现也有了,那么Java里面如何发现并加载服务实现呢?其实很简单,在resource/META-INF/services目录下,新建一个以服务接口全限定类名命名的文件,文件的内容就是具体实现类的全限定类名,r如果有多个实现类,则用换行进行分割:
# 文件位于:resource/META-INF/services/com.haoyanbing.spi.Log
com.haoyanbing.spi.FileLog
com.haoyanbing.spi.ELKLog
接下来,我们写个main方法来试一下:
public static void main(String[] args) {
// 使用Java提供的工具类ServiceLoader进行加载
ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
// 通过迭代器的方式遍历找到的具体实现类
Iterator<Log> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Log log = iterator.next();
log.log("hello world");
}
}
控制台输出如下:
FileLog: hello world
ELKLog: hello world
如输出所示,我们写的两个实现类都被ServiceLoader发现并加载到了。
SPI在JDBC中的应用
JDBC是Java中SPI应用的典型场景,我们来回顾下我们JDBC连接的样板代码:
Connection connection = null;
Statement statement = null;
ResultSet resultSet = null;
try {
// 注册驱动 从jdk6开始不需要这一行
// Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "username", "password");
// 获取Statement
statement = connection.createStatement();
// 执行查询SQL,并返回结果ResultSet
resultSet = statement.executeQuery("select * from t");
// 打印结果
while (resultSet.next()) {
System.out.println(resultSet.getString(1));
}
} finally {
// 资源关闭操作
if (resultSet != null) {
resultSet.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
}
从JDK6开始,我们不再需要注册驱动的那一行代码Class.forName("com.mysql.cj.jdbc.Driver")
也可以正常运行,就是靠SPI机制来实现的。
我们到源码里面去看一下具体如何实现的,我们到DriverManager.getConnection()
方法里面看一下:
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
// 这里调了重载的方法
return (getConnection(url, info, Reflection.getCallerClass()));
}
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
callerCL = Thread.currentThread().getContextClassLoader();
}
if (url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// 我们重点开这一行代码,这里就是确保驱动正确初始化了
ensureDriversInitialized();
SQLException reason = null;
for (DriverInfo aDriver : registeredDrivers) {
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.driver.getClass().getName());
}
}
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
private static void ensureDriversInitialized() {
if (driversInitialized) {
return;
}
synchronized (lockForInitDrivers) {
if (driversInitialized) {
return;
}
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty(JDBC_DRIVERS_PROPERTY);
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 就是在这里加载驱动
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 因为ServerLoader使用的是懒加载,要等到遍历该类的时候才会去加载
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers != null && !drivers.isEmpty()) {
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);
}
}
}
driversInitialized = true;
println("JDBC DriverManager initialized");
}
}
MySQL驱动实现类被加载的时候,会自己把自己注册到java的DriverManager里面,所有我们通过DriverManager.getConnection()
就可以获取到连接,我们看下MySQL实现的Driver类:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
// 该静态代码块,在类被加载的时候会执行
// 代码逻辑很简单,就是把自己注册到DriverManager里面去
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
后面通过DriverManager
获取Connection
的流程就不过多分析了。这就是SPI自动发现并加载JDBC驱动的整个流程。
ServiceLoader源码分析
知道了SPI的原理,也了解了Java中典型的应用场景,接下来我们具体看下ServiceLoader的源码,它是如何发现并加载类的。
按我们上面说的,其实不难想象,大致流程其实就是,ServiceLoader拿到需要加载的目标接口,然后在类路径扫描所有META-INF/services/xxx.xxx.xxx
的文件,再将文件里面的内容读取到,进行处理,再逐一加载类。
我们从ServiceLoader.load()
方法开始看:
// 只截取了相关代码,省略了无关代码
public final class ServiceLoader<S> implements Iterable<S>{
// 缓存加载的服务实现类
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒加载迭代器,加载类是通过这个来实现的
private LazyIterator lookupIterator;
// 我们调用的方法
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取线程上下文类加载器(后面会具体讲)
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用了重载的方法
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader){
// 直接创建了一个 ServiceLoader 对象并返回
return new ServiceLoader<>(service, loader);
}
// 构造方法,里面对参数做了一些校验然后调用了 reload()方法
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);
}
}
看到这里,我们发现这个时候只是创建了一个ServiceLoader对象,并没有执行任何的加载,所以ServiceLoader是懒加载的,等到遍历的时候才会去加载,这也就是上面JDBC里面加载完要遍历一下的原因了。
那我们再看一下hasNext()
和next()
方法:
public Iterator<S> iterator() {
// 返回一个匿名内部类
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 这里主要调的是lookupIterator里面的hasNext方法
public boolean hasNext() {
// 这个判断是已经加载过了才进这个判断,我们第一次加载providers是空的,所以进不了这个判断
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
// 同样,next调用的也是lookupIterator里面的next方法
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
那我们到lookupIterator
这个类里面去看看吧:
// 这个是个私有的内部类,实现了对服务实现的懒加载,为了方便阅读,把方法的顺序做了调换
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;
}
// 这个方法调用了内部的hasNextService方法
// 里面的if-else判断主要是安全策略的逻辑,我们暂不去管他,
// 不管if还是else最终都是调用hasNextService方法
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private boolean hasNextService() {
// 初始nextName为空
if (nextName != null) {
return true;
}
// 初始configs也为空
if (configs == null) {
try {
// 这个的常量 PREFIX = "META-INF/services/"
// fullName就是我们需要查询的文件名
String fullName = PREFIX + service.getName();
// 这里查询所有符合名称条件的文件,返回的是Enumeration<URL>
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;
}
// 这个调用了内部的parse方法,这个方法很简单就是把文件内容读取并返回一个Iterator<String>
// 不是直接返回String而是Iterator<String>,是因为文件里面可以写多个实现类
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
// next方法也是直接调用了nextService方法
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
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 subtype");
}
// 实例化该实现类并放入到providers中进行缓存
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
}
public void remove() {
throw new UnsupportedOperationException();
}
}
到这里整个服务的发型和加载流程就走完了,整个流程还是非常简单和清晰的。
线程上下文加载器
这里面设计到一个类加载器的知识,这里简单提一下。Java里面一共有四种类加载器,类加载器之间是有父子关系的,按照从上到下分别是:BootstrapClassLoader、ExtClassLoader、AppClassLoader和用户自定义的类加载器。
每个加载器能加载类的范围是有限制的,例如:BootStrapClassLoader只能加载jdk自己的类,不能加载用户自己编写的类和第三方jar包里面的类。
类加载器里面有一条规则,是双亲委托机制,就是每个加载器在加载一个类的时候,首先是交由自己的父加载器进行加载,如果父加载器加载不了再由子加载器加载。而且一个类去加载另外一个类的时候,默认是使用加载自己的类加载器去加载。
回到我们的SPI上面,ServiceProvider类去加载各个厂商的具体实现时,如果不指定类加载器,直接去加载则是用的BootStrapClassLoader这个类加载器,但是这个类加载器是不能加载三方jar包里面的类的。那么这样SPI岂不是没办法玩了。
所以Java提供了线程上下文类加载器,也就是可以从线程上下文里面获取类加载器来加载类,这样就绕过了BootStrapClassLoader不能加载三方类的限制。
关于类加载器相关的知识大家可以查询资料再详细了解,这里不做展开说明。
总结
本文先介绍了SPI的概念,然后通过简单的代码示例,来说明SPI如何使用。接着分析了JDBC这个在Java中典型的SPI应用场景,然后我们把ServiceLoader的源码也进行了详细的分析,最后也提到了线程上下文类加载器的相关知识。通过概念讲解,代码示例分析,典型应用场景分析,源码分析等多个方面来说明SPI机制,算是比较详细的了,希望对大家理解SPI能有所帮助!