Java SPI机制
什么是SPI
在编码过程中,我们经常会按照上图的方式进行编码,通常会定义一个接口,然后定义该接口的实现类。在使用时会“父类引用指向子类对象”的方式使用。这是在类的层面比较直观的一种展示。我们不妨在放开眼界,在模块,应用层面思考一下。
我们对上面类关系进行放大,我们一个模块或应用依赖一组接口,比如JDBC、日志框架等
上图我们发现接口是介于调用方和具体实现的中间层,那什么时候接口更偏向与调用方,什么时候接口更偏向与实现方???
- 接口属于实现方的情况,也就是实现方提供了接口和实现,我们可以引用接口达到调用某实现类的功能,这就是我们常说的API,它具有以下特征:
- 概念上更接近实现方
- 组织上位于实现方所在的包中
- 实现和接口在一个包中
- 接口属于调用方时,我们称其为SPI,全称service provider interface
- 概念上依赖调用方
- 组织上位于调用方所在的包中
- 实现位于独立的包中
可能上面的解释还是比较难以理解,那我们就说点人话。我们假设接口就是一种服务,只不过它不提供真正服务,只是定义了功能。
- 所谓SPI是站在 服务使用方 角度提出的“接口要求”,是对“服务提供方”提出的约定,简单说就是:“我需要这样的服务,现在你们来满足”。
- 所谓API是站在 服务提供方 角度提供的无需了解底层细节的操作入口,即“我有这样的服务可以给你使用”。
为什么使用SPI
在第一节中,我们说过“父类引用指向子类对象”的方式,这种方式还是存在“硬编码”,而SPI提供了一种机制使得使用方与接口实现方完全解耦(后面介绍)。
作为开发人员,我们知道需求一直在变,所以开发人员一直在追求当需求在发生变更时我如何很少改动代码,甚至不改动代码就能满足变化后的需求。或者我只进行扩展,而不是修改代码(开闭原则)。而SPI就是一种只需引入新的实现,不需更改一行代码,就能彻底替换掉底层的实现。
通过上面的图示,我们可以看到应用调用方和接口始终是不变的,而变化的仅仅是接口的实现方,这样就保证了只要我们依赖的接口不发生改变,我们程序是不需要改动的,一般接口都是顶级抽象,都是比较稳定的,很少发生改变。
其实SPI的思想就是解耦,只不过它是完全解耦,提供了一种“可插拔”的方式。
SPI原理
在jdk6中引入了一种机制,提供了一个ServiceLoader类,通过官方API文档可以知道:它主要是用来装载一系列的service provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
模拟官方Demo
创建接口(服务)
public interface CodecSet {
Encoder getEncoder(String encodingName);
Decoder getDecoder(String encodingName);
}
//提供解码功能的接口
interface Decoder {}
//提供编码功能的接口
interface Encoder {}
创建接口的实现(服务的具体实现)
public class StandardCodecs implements CodecSet {
@Override
public Encoder getEncoder(String encodingName) {
return new DefaultEncoder();
}
@Override
public Decoder getDecoder(String encodingName) {
return new DefaultDecoder();
}
}
//编码的默认实现
class DefaultEncoder implements Encoder {}
//解码的默认实现
class DefaultDecoder implements Decoder {}
创建配置文件(发现服务的约定)
- 在classpath创建META-INF/services/目录
- 创建一个名字为com.keminapera.jdkapi.spi.CodecSet文件(该文件名就是需要加载服务接口的全限定名称)
- 文件的内容就是接口的具体实现类的全限定名称
创建工厂(服务发现机制)
public class CodecsFactory {
private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);
public static Encoder getEncoder(String encodingName) {
for (CodecSet cp : codecSetLoader) {
Encoder enc = cp.getEncoder(encodingName);
if (enc != null) {
return enc;
}
}
return null;
}
}
测试类
public class SpiTest {
@Test
public void test() {
Encoder encoder = CodecsFactory.getEncoder(StandardCharsets.UTF_8.name());
System.out.println(encoder);
}
}
通过模拟官方的demo,会发现,我们没有创建任何对象,仅仅通过
CodecsFactory
类我们就能获取到我们想要的实例对象,CodecsFactory
类里面通过JDK提供的ServiceLoader
类实现的,我们一起看看ServiceLoader
类的具体实现。
ServiceLoader类
官方接口文档
其实官方API文档已经写的很清楚了,总共对外提供了5个方法:
- iterator():Iterator :以懒加载的方加载可用的providersServiceLoader
- load(Class<> service):ServiceLoader:通过给定的类型创建了一个
ServiceLoader
对象,默认使用当前线程的上下文类加载器加载 - load(Class<> service, ClassLoader loader):ServiceLoader:通过给定类型创建一个
ServiceLoader
对象,使用给定的类加载器加载 - loadInstalled(Class<> service): ServiceLoader:通过给定类型创建一个
ServiceLoader
对象,使用扩展类加载器加载 - reload():void:清除
ServiceLoader
缓存并重新加载
通过给定的API,我们可以发现其中有3个都是提供的静态工程方法用来创建
ServiceLoader
对象的,只有iterator和reload方式才是真正去加载provider,所以我们重点看看这两个方法的实现
核心代码
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//private static final String PREFIX = "META-INF/services/"; //定义的前缀
//Class<S> service; 就是传入需要加载的接口对应的Class
String fullName = PREFIX + service.getName();
//ClassLoader loader; loader指定类加载器
//Enumeration<URL> configs = null; 需要加载的文件路径
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//Iterator<String> pending = null;
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// String nextName = null;
nextName = pending.next();
return true;
}
JDBC驱动加载
添加MySql依赖
通过上面SPI原理的分析,我们看看JDBC中是如何使用SPI的,我们引入mysql依赖包,在依赖包中找到/META-INF/services/java.sql.Driver文件,该文件存放的就是需要加载的“服务提供者”。
MySql中Driver实现类
该类在静态代码块中仅仅向DriverManager
类里面注册了一个驱动
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver()); //注册驱动
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
原始JDBC方式注册驱动
想想之前我们使用JDBC编程的是什么样子???是不是首先需要通过Class.forName()注册驱动
public class Main {
//第二步:说明JDBC驱动的名称和数据库的地址
static final String JDBC_DRIVER = "com.mysql.jdbc.Driver";
static final String DB_URL = "jdbc:mysql://localhost:3306/test";
//第三步:说明数据库的认证账户及密码
static final String USER = "root";
static final String PASS = "123456";
public static void main(String[] args) {
//第四步:注册JDBC驱动
try {
Class.forName(JDBC_DRIVER);
} catch (ClassNotFoundException e) {
//这里会发生类没有找到的异常!
e.printStackTrace();
}
//第五步:获得数据库连接
try {
Connection connection = DriverManager.getConnection(DB_URL,USER,PASS);
//第六步:执行查询语句
Statement statement = connection.createStatement();
String sql = "SELECT * FROM crawler_article";
ResultSet rs =statement.executeQuery(sql);
while (rs.next())
{
String title = rs.getString("title");
String author = rs.getString("author");
System.out.println(title+":"+author);
}
//第七步:关闭连接资源
rs.close();
statement.close();
connection.close();
} catch (SQLException e) {
e.printStackTrace();
//这里会发生SQL异常,因为我们提供的的账户和密码不一定能连接成功
}
}
}
原始JDBC注册驱动大致流程
SPI方式注册驱动
public class DriverManager {
//当该类被加载到JVM--在初始化阶段会执行静态代码块
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;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); //使用到了SPI机制
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);
}
}
}
}
通过对上面流程分析,我们可以得到下面的一个大致流程图