1、什么是SPI
SPI全称为 Service Provider Interface ,直译为服务提供者接口,源自服务提供者框架(Service Provider Framework),是一种将服务接口与服务实现分离以达到解耦、提升了程序可扩展性的机制。
在Java中一个非常典型实例是:JDBC
sun公司为了实现Java连接各大数据库,编写了JDBC接口,由各家数据库编写实现。
回忆一下数据连接六步:
- 加载驱动
- 获取连接
- 获取SQL执行器
- 执行SQL
- 获取结果集
- 关闭连接
以MySQL为例,对应的代码如下
public class TestJDBC {
public static void main(String[] args) throws ClassNotFoundException, SQLException {
// 这一步可以省略!
Class.forName("com.mysql.cj.jdbc.Driver");
// 获取连接
Connection connection = DriverManager.getConnection("jdbc:mysql://localhost:3306/testdb","root","123");
// 获取SQL执行器
Statement statement = connection.createStatement();
// 执行SQL,获取查询结果集
ResultSet resultSet = statement.executeQuery("SELECT * FROM users");
// 关闭连接
connection.close();
}
}
为什么说类加载可以省略?
但是我想在知道原因之前你应该还有一个疑问就是,为什么类加载这一步可以加载驱动?
其实严格来说应该叫注册驱动,在com.mysql.cj.jdbc
包下找到Driver
这个类,你会发现一个静态代码块。
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
我们都知道,静态代码块在类加载的时候执行,这一步就会帮我们将驱动注册好,也就是将JDBC接口和MySQL的实现绑定,我们只需要调用接口就可以了。
如何注册的?
进入DriverManager
这个类,里面有一段静态代码块:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
上面的意思是JDBC的DriverManage已经初始化完成!那肯定和loadInitialDrivers()
有关,点开它,我发现这么一段代码
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
PS:
ServiceLoader
是JDK6开始提供的一个实现SPI的类,说白了,就是给它一个接口,它来帮你找实现类,如何找在下面再解释!
现在可以说,为什么Class.forName("com.mysql.cj.jdbc.Driver");
可以省略了。
在MySQL驱动 5.1.6 以后可以无需 Class.forName(“com.mysql.cj.jdbc.Driver”);使用了SPI机制,会自动去发现服务的实现。
在Java中,SPI是去在Classpath下的META-INF/services
发现接口的实现类的,以接口的全类名作为文件名,文件中每一行是这个接口实现类的全类名。
例如上图JDBC中的Driver全类名作为文件名,其中内容为
com.mysql.cj.jdbc.Driver
这样在程序启动的时候就会自动加载com.mysql.cj.jdbc.Driver
这个类!
2、Java中是如何实现的SPI
实现的机制在上面已经提到了,我们这里就编写一个例子验证!
2.1 模拟JDBC
创建一个普通java项目作为接口包(你也可以用maven)
package cn.butcher;
/**
* 驱动接口,其中有一个获取连接的方法
*/
public interface MyDriver {
void getConnection();
}
是的整个项目就只有这一个接口类~
然后打成jar包,打包方法可百度哈,我这里jar的名字为:bt-driver.jar
2.2 模拟mysql驱动
同样创建一个普通的java项目,导入bt-driver.jar
,创建自己的实现类
package com.butcher;
import cn.butcher.MyDriver;
public class MyDriverImpl implements MyDriver {
@Override
public void getConnection() {
System.out.println("hello, this is MyDriverImpl!");
}
}
为了区分,我使用了不同的包名,以说明这也接口不是同一个项目。
然后在classpath下创建META-INF/services
文件夹,创建文件名为cn.butcher.MyDriver
的文件,在其中加入实现类的全限定名
com.butcher.MyDriverImpl
最终项目结构:
ok,打成jar包 :butcher-driver.jar
2.3 模拟在自己项目中使用
创建一个项目,先引入bt-driver.jar
这个依赖,然后创建一个类测试
import cn.butcher.MyDriver;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Test {
public static void main(String[] args) {
// 使用ServiceLoader加载META-INF/services下的配置的服务
ServiceLoader<MyDriver> loader = ServiceLoader.load(MyDriver.class);
// 获取迭代器,也就是可以有多个接口的实现,这里我只拿一个,拿多个用while即可
Iterator<MyDriver> iterator = loader.iterator();
if (iterator.hasNext()) {
MyDriver driver = iterator.next();
driver.getConnection();
} else {
System.err.println("no such MyDriver implement !");
}
}
}
运行结果:
no such MyDriver implement !
没有找到实现类?当然找不到,因为我们实现类都没有引入。这时候引入butcher-driver.jar
代码不变,再次运行:
hello, this is MyDriverImpl!
有两个坑说明一下
- 如果使用idea进行打包,那么重新打包的时候需要将你之前打包的删掉,不然有可能出现一些诡异的现象。
- 如果使用idea创建
cn.butcher.MyDriver
这个文件的时候选文件类型,选Text类型就可以了,这并不会将这个文件类型修改为.txt
文件,而是告诉idea,虽然你不知道这个文件的类型,但请你以文本的形式给我打开就行了!
3、总结
OOP七大原则中有一条,面向接口编程,JDBC就是一个很好例子,如果我们把JDBC写死了,那切换数据的时候就会非常麻烦,类似于slf4j
也是一样的初心。
Java的SPI类似于IOC的功能,将装配的控制权移到了程序之外,实现在模块装配的时候不用在程序中动态指明。所以SPI的核心思想就是解耦,这在模块化设计中尤其重要。