SPI机制简介

SPI的全名为Service Provider Interface.java spi机制的思想: 系统里抽象的各个模块,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。

一个比较容易理解的例子:JDK中有支持音乐播放,假设只支持mp3的播放,有些厂商想在这个基础之上支持mp4播放,有的想支持mp5播放,而这些厂商都是第三方厂商,如果没有提供SPI这种实现标准,那就只有修改JAVA的源代码了,那这个弊端也是显而易见的,而有了SPI标准,SUN公司只需要提供一个播放接口,在实现播放的功能上通过ServiceLoad的方式加载服务,那么第三方只需要实现这个播放接口,再按SPI标准的约定进行打包,再放到classpath下面就OK了,没有一点代码的侵入性。

浅析SPI_mysql

SPI具体约定

java spi的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

SPI 实现原理

应用进程调用 ServiceLoader.load 方法 ServiceLoader.load 方法内先创建一个新的 ServiceLoader,并实例化该类中的成员变数,包括:

应用进程通过迭代器获取对象实例 ServiceLoader 先判断成员变量 providers 对象中否有缓存实例对象,如果有缓存,直接返回。

如果没有缓存,执行类的装载:读取 META-INF/services/ 下的配置文档,获得所有能被实例化的类的名称,通过反射方法 Class.forName() 载入类对象,并用 instance() 方法将类实例化。把实例化后的类缓存到 providers 对象中然后返回实例对象。

JDBC中的SPI机制

浅析SPI_java_02

在java中,Java.sql.Driver接口是Java对外公开的一个加载驱动接口,Java并未实现,至于实现这个接口由各个Jdbc厂商去实现就行了,好处是解藕,使得更具有灵活性,当然这也是面向对象的好处之一。真正的实现是不同提供商提供的。

Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "123456");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from Users");

Class.forName("com.mysql.jdbc.Driver");这里虽是加载mysql的driver,但是无论是oracle还是其它的jdbc驱动包,它们的原理都是spi机制。首先看java.sql.driver

package java.sql;

import java.util.logging.Logger;
public interface Driver {
boolean acceptsURL(String url) throws SQLException;

DriverPropertyInfo[] getPropertyInfo(String url, java.util.Properties info)
throws SQLException;

int getMinorVersion();

boolean jdbcCompliant();

public Logger getParentLogger() throws SQLFeatureNotSupportedException;
}

这个类是一个接口类, 在整个JDK包中都没有它的实现类,它的实现要由各个jdbc的开发产商去实现,但是我们发现DriverManager这个类中还是有去加载Driver类,那它是怎么发现其它开发商实现的Driver类?

再来看看DriverManager

这里将driver封成一个driverinfo对象,然后在DriverManager 这个类加载时就初始化一次,那初始化它做什么呢?其实就是其发现driver类的实现类,并加载到当前类中。注意看loadInitialDrivers方法。

public class DriverManager {


// List of registered JDBC drivers
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<DriverInfo>();

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);
Iterator 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);
}
}
}
...
}

ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);这一句就是真实的发现driver类的实现类。

在mysql-connector-java-.jar包下面META-INF.services包下有个java.sql.Driver文件打开文件有下面两行

浅析SPI_java_03

com.mysql.jdbc.Driver这样就实现了在我们程序运行时引入mysql-connector-java-.jar这个包。然后在JDK源码中,当我们调用到

DriverManager.getConnection的方法,就会将程序中使用到的Driver接口类用mysql驱动包的Driver实现类来使用。

这样做有什么好处?

1、不用在JDK里实现Driver实现类的硬编码,然后每次使用JDK里的DriverManager 类时,都会自动去发现Driver类的实现类,并根据这个实现类来做数据库连接。

2、可以满足不同的产商实现各不相同,但对外暴露一样的接口。使用方只要按照JDK的标准方法来调用即可,即实现了接口的可拔插。

应用场景

  • JDBC加载不同类型的驱动
  • SLF4J对log4j/logback的支持
  • Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对java提供的原生SPI做了封装

优缺点

优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。

缺点

不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。多个并发多线程使用 ServiceLoader 类的实例是不安全的。加载不到实现类时抛出并不是真正原因的异常,错误很难定位。


关注公众号 soft张三丰 

浅析SPI_实例化_04