文章目录
什么是JDK SPI
SPI全称Service Provider Interface,是JDK内置的一种服务提供发现机制,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
例如,使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口,不同数据库产品底层的协议不同,提供的 java.sql.Driver 实现也不同,在开发 java.sql.Driver 接口时,开发人员并不清楚用户最终会使用哪个数据库,在这种情况下就可以使用 Java SPI 机制在实际运行过程中,为 java.sql.Driver 接口寻找具体的实现。
JDK SPI 机制
当服务的提供者提供了一种接口的实现之后,需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时,JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名,进行实现类的加载和实例化,最终使用该实现类完成业务功能。
所以spi需要几个关键点:
- 有一个接口
- 在 Classpath 下的 META-INF/services/ 创建一个以服务接口命名的文件
- 在文件内写上对应实现的全类名
SPI 简单实现
通过一个简单的示例演示下 JDK SPI 的基本使用方式:
- 有一个接口
创建一个Log接口:
public interface Log {
void log(String info);
}
-
在 Classpath 下的 META-INF/services/ 创建一个以服务接口命名的文件
创建对应的文件
-
在文件内写上对应实现的全类名
写两个实现:
log4j实现:
public class Log4j implements Log {
@Override
public void log(String info) {
System.out.println("这是Log4j实现:" + info);
}
}
logback实现:
public class Logback implements Log {
@Override
public void log(String info) {
System.out.println("这是Logback实现:" + info);
}
}
写个main方法执行对应实现:
public class SpiMain {
public static void main(String[] args) {
ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);
for (Log log : serviceLoader) {
log.log("益达测试 SPI");
}
}
}
执行结果:
这是Log4j实现:益达测试 SPI
这是Logback实现:益达测试 SPI
源码分析
初始化
spi主要是通过java.util.ServiceLoader这个类:
public final class ServiceLoader<S>
implements Iterable<S>
{
// 文件路径
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
// 加载的服务类或接口类
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
// 已加载的服务类集合
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 真正加载服务类
private LazyIterator lookupIterator;
public void reload() {
// 清空
providers.clear();
// 实例化内部加载服务类
lookupIterator = new LazyIterator(service, loader);
}
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();
}
load方法创建了一些属性(要加载的接口,类加载器,访问控制器),然后执行reload方法,reload主要实例化了内部类LazyIterator。load最后返回ServiceLoader的实例。
查找实现类
查找实现类和创建实现类的过程,都在LazyIterator完成。当我们调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的iterator方法,iterator里面又是执行hasNext和next等方法,
hasNext主要执行hasNextService方法,
next方法主要执行nextService方法,
我们看看hasNextService和nextService方法:
private boolean hasNextService() {
// 第二次调用已经解析完成直接返回true
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
//META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
String fullName = PREFIX + service.getName();
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;
}
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 {
//反射加载类
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
}
到nextService的时候通过反射将对应的类进行返回.
应用场景
数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动
数据库DriverManager类,它在静态代码块里面已经通过SPI机制, 把数据库驱动连接初始化了。它在里面查找的是Driver接口的服务类,所以它的文件路径就是:META-INF/services/java.sql.Driver
public class DriverManager {
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
// 省略部分代码
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
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.jdbc.Driver类中,它做的主要就是在注册自身:
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
我们可以通过自定义Driver类来实现一些简单的扩展,在获取连接前打印连接信息:
新增自定义YidaDriver,我们需要继承原来的Driver,然后在resources下新建:
META-INF.services.java.sql.Driver文件文件内容写上我们自定义的com.study.springbootplus.config.YidaDriver全路径,
还需要将项目中配置的driver-class-name换成我们自定义的:
import com.mysql.jdbc.NonRegisteringDriver;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.util.Properties;
import java.util.logging.Logger;
/**
* @ClassName YidaDriver
* @Author yida
* @Date 2021/11/29 8:20 下午
* @Description YidaDriver
*/
public class YidaDriver extends NonRegisteringDriver implements Driver {
static {
try {
java.sql.DriverManager.registerDriver(new YidaDriver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException if a database error occurs.
*/
public YidaDriver() throws SQLException {
System.out.println("yida");
}
@Override
public Connection connect(String url, Properties info) throws SQLException {
System.out.println("准备创建数据库连接.url:"+url);
System.out.println("JDBC配置信息:"+info);
info.setProperty("user", "root");
Connection connection = super.connect(url, info);
System.out.println("数据库连接创建完成!"+connection.toString());
return connection;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
最终启动打印日志出现:
yida
准备创建数据库连接.url:jdbc:mysql://localhost:3306/test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true
JDBC配置信息:{user=root, password=root}
数据库连接创建完成!com.mysql.jdbc.JDBC4Connection@30ffb2a6
日志门面接口实现类加载
SLF4J加载不同提供应商的日志实现类
Spring
Servlet容器启动初始化org.springframework.web.SpringServletContainerInitializer
Spring Boot
自动装配过程中,加载META-INF/spring.factories文件,解析properties文件
Dubbo
Dubbo大量使用了SPI技术,里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来
例如Protocol 协议接口