SPI机制
什么是SPI?
SPI,全称Service Provider interface
,即服务提供发现接口。(本质上就是面向接口编程)
它能通过在classpath/META-INF/services文件夹中查找文件,自动加载文件中定义的类。
Tips1: 虽然平时并不会直接使用到 SPI 来实现业务,但其实我们使用过的绝大多数框架都会提供 SPI 接口方便使用者扩展自己的功能。
常见的如Dubbo,它提供了一系列的拓展
此外在JDBC中也使用了SPI机制
例子
我们先定义一个接口(我们通过该接口发现路径下的相关服务)
public interface SPIService{
void excute();
}
定义两个服务A,B,实现SPIService接口
public class A implements SPIService{
void excute(){
System.out.println("A执行");
}
}
public class B implements SPIService{
void excute(){
System.out.println("B执行");
}
}
最后我们在classpath/META-INF/services
文件夹中
文件名为接口的全限定名: com.test.spi.service.SPIService
文件内容为接口实现类的全限定类名,多个用换行符隔开:
com.test.spi.service.Impl.A
com.test.spi.service.Impl.B
这样子我们可以根据SPI机制,**通过读取相关路径信息,启动所需要的服务。**写个测试类
我们有两种方式可以实现,一种是通过ServiceLoad.load()
方法,由java.util
包提供
另一种是通过Service.providers
方法拿到实现类的实例,由sum.misc.Service
提供
public class TestSPI {
public static void main(String[] args) {
FirstCallOfSPI();
System.out.println("---------------");
SecondCallOfSPI();
}
//Service.providers()
private static void FirstCallOfSPI() {
Iterator<SPIService> providers =
Service.providers(SPIService.class);
while (providers.hasNext()) {
SPIService next = providers.next();
next.excute();
}
}
//ServiceLoader.load()
private static void SecondCallOfSPI() {
ServiceLoader<SPIService> load =
ServiceLoader.load(SPIService.class);
Iterator<SPIService> iterator = load.iterator();
while (iterator.hasNext()) {
SPIService next = iterator.next();
next.excute();
}
}
}
执行结果
源码解析
这里主要说一下ServiceLoad,由java.util
提供(sum.misc.Service看不了源码)
ServiceLoad类
load()
这步主要是实例化了内部类:LazyIterator
,并返回ServiceLoader的实例
Tips2: AccessController的作用(看源码最害怕多几个不认识的名词了。。)
简单介绍下 // todo 有机会深入了解Java安全策略的话就会更懂了
AccessController
类用于与访问控制相关的操作和决定。
更准确的说:AccessController
类用于以下三个目的
- 基于当前的Java安全策略决定是允许还是拒绝对关键系统资源的访问
- 将代码标记为享有“特权”,从而影响该代码后续的资源访问决定
- 获取当前调用上下文的”快照",这样就可以相对于以保存的上下文做出其他上下文的访问控制决定。
一般通过调用checkPermission()
根据特定算法进行权限判定
我们可以通过doPrivileged
将调用方标记为特权,只要调用方访问拥有权限的域,就不需要checkPermission权限判定了;但是如果过域了,通常直接抛出异常。
LazyIterator的内部细节
LazyIterator内部主要是查找实现类和创建实现类的过程。
其实当我们调用Iterator.hasNext()
和Iterator.next()
的时候,都是在调用LazyIterator的相应方法
其中Iterator.hasNext()
主要用于查找实现类
而Iterator.next()
主要用于创建实例
所以现在我们更加关注的是,LazyIterator是怎么重写这两个方法来进行实现类的查找和创建呢?
Tips3: PrivilegedAction的作用?
PrivilegedAction
是一个具有单个方法的接口public interface PrivilegedAction<T> { T run(); }
方法名为run,并返回一个Object。
一般该接口要配合AccessController进行权限控制代码进行使用,大概使用场景:
aboutingmethod(){ // 普通代码写在这。。。 AccessController.doPrivileged(new PrivilegedAction(){ public Object run(){} //特权相关代码在这里编写,比如: System.out.println("awt"); return null; } }); //普通代码... }
上述事例显示了该接口的实现的创建,提供了run方法的具体实现。
调用
doPrivileged
时,将PrivilegedAction
实现的实例传递给它。
doPrivileged
方法在启用特权后从 PrivilegedAction 实现调用run
方法,并返回run
方法的返回值作为doPrivileged
的返回值、在使用“特权”构造时务必 * 特别 * 小心,始终让享有特权的代码段尽可能的小。
查找实现类
调用Iterator.hasNext()
最重要就是调用hasNextService()
方法
创建实例
调用next方法的时候,实际调用的是lookupIterator.nextService()
。
它通过反射的方式,创建实现类的实例并返回。
到此,SPI机制如何读取/META-INF/services/目录下的文件
并将文件中的全限定类名进行初始化,获取到类的实例的一系列过程,我们都进行了简单的了解。
JDBC中的应用
SPI机制为很多框架的拓展提供了帮助和可能,在JDBC中就应用到了该机制。
在最初的JDBC,获取数据库连接的过程,需要先设置数据库驱动的连接,在通过DriverManager.getConnection
获取一个Connnection
String url = "jdbc:mysql:///xxx"
String user = "root";
String password = "root";
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(url, user, password);
在新版本中,设置数据库驱动连接,这一步骤就不需要了,而是通过SPI机制进行配置
加载
我们首先看看DriverManager
类,它在静态代码块做了一件重要的事
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hqMsUNZ2-1630160091127)(SPI机制/image-20210705192025700.png)]
显然,它已经通过SPI机制,将数据库驱动连接初始化了。
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() {
//眼熟吧,SPI机制加载Driver接口的服务类,Driver接口的包为:java.sql.Driver
//所以它要找的就是META-INF/services/java.sql.Driver文件
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);
}
}
}
获取Connection
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
/*
* When callerCl is null, we should check the application's
* (which is invoking this class indirectly)
* classloader, so that the JDBC driver class outside rt.jar
* can be loaded from here.
(当callerCl是空,我们应该检查间接调用类的应用程序加载器,以便JDBC驱动器能够加载rt.jar外的类)
*/
ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) {
callerCL = Thread.currentThread().getContextClassLoader();
}
}
if(url == null) {
throw new SQLException("The url cannot be null", "08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
//这一段就是加载的核心
////registeredDrivers中就包含com.mysql.cj.jdbc.Driver实例
for(DriverInfo aDriver : registeredDrivers) {
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
//调用connect方法创建连接
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.getClass().getName());
}
}
// if we got here nobody could connect.
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");
}
总结
Java
自身的 SPI
其实也有点小毛病,比如:
- 遍历加载所有实现类效率较低。
- 当多个
ServiceLoader
同时load
时会有并发问题(虽然没人这么干),所以在JDBC上有一串对类加载器的处理,不知道是不是为了解决这个问题
最后总结一下,SPI
并不是某项高深的技术,本质就是面向接口编程,而面向接口本身在我们日常开发中也是必备技能,所以了解使用 SPI
也是很用处的。