深入剖析Java SPI机制
1. 什么是SPI
SPI(Service Provider Interface,服务提供者接口),比如:java.sql.Driver接口,不同厂商针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
SPI经典的实现就是JDK1.6引进的 final类: java.util.ServiceLoader
官方注释
* A simple service-provider loading facility.
*
* <p> A <i>service</i> is a well-known set of interfaces and (usually
* abstract) classes. A <i>service provider</i> is a specific implementation
* of a service. The classes in a provider typically implement the interfaces
* and subclass the classes defined in the service itself. Service providers
* can be installed in an implementation of the Java platform in the form of
* extensions, that is, jar files placed into any of the usual extension
* directories. Providers can also be made available by adding them to the
* application's class path or by some other platform-specific means.
大概意思是:ServiceLoader是一个简单的服务提供者加载设备,主要用来装载一系列的service
provider。 而且ServiceLoader可以通过service
provider的配置文件来装载指定的service
provider。说了这么多是否还是一头雾水,下面先看个简单的例子,主要有4个类:
IServiceLoader (接口)
AServiceLoaderImpl (实现类A)
BServiceLoaderImpl (实现类B)
ServiceLoaderTest (测试类)
步骤:
- 创建一个接口文件
- 在resources资源目录下创建META-INF/services文件夹
- 在services文件夹中创建文件,以接口全名命名,eg:com.xxx.test.spi.IServiceLoader,在文件中添加接口的实现类,全名
- 创建接口实现类
- ServiceLoader还有一个特定的限制,就是我们提供的这些具体实现的类必须提供无参数的构造函数,否则ServiceLoader就会报错。
IServiceLoader.java 该接口就提供一个简单的方法 say()
public interface IServiceLoader {
String say();
}
AServiceLoaderImpl.java
public class AServiceLoaderImpl implements IServiceLoader {
@Override
public String say() {
return "A say()";
}
}
BServiceLoaderImpl.java
public class BServiceLoaderImpl implements IServiceLoader {
@Override
public String say() {
return "B say()";
}
}
ServiceLoaderTest.java
import java.util.ServiceLoader;
public class ServiceLoaderTest {
public static void main(String[] argus) {
ServiceLoader<IServiceLoader> serviceLoader = ServiceLoader.load(IServiceLoader.class);
Iterator<IServiceLoader> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
IServiceLoader next = iterator.next();
System.out.println(next + " : " + next.say());
}
}
}
META-INF/services下的 com.xxx.test.spi.IServiceLoader 文件,内容如下,两个实现类的全称
com.xxx.test.spi.AServiceLoaderImpl
com.xxx.test.spi.BServiceLoaderImpl
执行的结果值
com.xxx.test.spi.AServiceLoaderImpl@6fffcba5 : A say()
com.xxx.test.spi.BServiceLoaderImpl@2aafb23c : B say()
以上就是一个SPI的实现,整个例子里面我们并没有去直接new出接口IServiceLoader的实现类,最终却把实现类的结果都输出,原因是什么?先看看ServiceLoader load()方法的源码
/**
* Creates a new service loader for the given service type, using the
* current thread's {@linkplain java.lang.Thread#getContextClassLoader
* context class loader}.
*
* <p> An invocation of this convenience method of the form
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>)</pre></blockquote>
*
* is equivalent to
*
* <blockquote><pre>
* ServiceLoader.load(<i>service</i>,
* Thread.currentThread().getContextClassLoader())</pre></blockquote>
*
* @param <S> the class of the service type
*
* @param service
* The interface or abstract class representing the service
*
* @return A new service loader
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
通过源码我们知道,load()方法会把IServiceLoader的所有子类型都装载到JVM中并可以使用,
那么java是如何找到他的子类而且有且仅有两个子类呢?再看看前面定义的META-INF下面的配置文件,
是不是就可以发现java通过查找META-INF目录下对应IServiceLoader全名的文件,然后把文件里所有的类全名并装载它们
ServiceLoader使用的思考
目前来说,我们已经看到了ServiceLoader的一种简单使用场景。在前面这个示例中,我们对接口的实现是在同一个工程里面,
如果我们需要使用他们的时候,完全没必要通过ServiceLoader再装载具体实现进来,我们完全可以通过一个ArrayList再将他们的具体实现一个个加进去就可以了。
那么,什么时候用ServiceLoader比较合适呢?既然在同一个工程里我们jvm可以直接装载他们的实现,那么很可能就是我们要装载的实现不在同一个工程里,
可能是需要我们动态添加的,这个时候,他们的引入不是编译时候加进来的,是在运行的时候加入的,我们不能像使用普通引入的静态类库那样来使用他们。
所以这就是ServiceLoader的优点所在了。比如说我们有一组接口,有的实现是我们本地的,我们可以在使用代码里直接引入进来。而有的却是第三方实现的,
他们可能会在运行的时候加入进来。那么我们事先是不清楚的,也就不可能定死了他们的实现是哪个具体的类名。在前面ServiceLoader的使用里,
我不用把你具体的实现引用到代码里,而只是在配置文件里指定就可以了。这一点也是我们后面要讨论的DriverManager的一个重要的核心思想。
DriverManager的应用和设计思路
DriverManager是jdbc里管理和注册不同数据库driver的工具类。从它设计的初衷来看,和我们前面讨论的场景有相似之处。首先一个,针对一个数据库 可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时不希望修改现有的代码才能达到目的,而希望通过一个简单的配置就可以达到效果。
比如说,我们现在有一个数据库的驱动A,我们希望在程序里使用它而不修改代码。一种理想的选择就是我们将驱动A的信息加入到一个配置文件中,程序通过读取配置文件信息将A加载进来。而以后如果我们希望改用另外一个驱动B的时候,我们之需要将配置文件里的信息修改成驱动B的。我们肯定不希望在代码里写什么registerDriver(new A());之类的代码。尤其在有的情况下我们根本没有使用这些驱动的源代码。
这里mysql JDBC 的实例来阐述SPI以及DriverManager的思想
首先在工程中加入 mysql-connector-java 依赖
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>
申明一个测试类,使用 DriverManager.getConnection(url, username, password);去连接数据库
public class MysqlTest {
public static void main(String[] args) throws SQLException {
Connection connection = null;
try {
String url = "jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&createDatabaseIfNotExist=true";
String username = "root";
String password = "123456";
String sql = "select * from user;";
connection = DriverManager.getConnection(url, username, password);
Statement statement = connection.createStatement();
final ResultSet rs = statement.executeQuery(sql);
while (rs.next()) {
int id = rs.getInt(1);
String name = rs.getString(2);
System.out.println("id = " + id + ", name = " + name);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if(null != connection) {
connection.close();
}
}
}
}
DriverManager.getConnection(),对DriverManager类的主动使用,进入DriverManager源码
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
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();
/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
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);
}
}
}
上面的源码中可以看到DriverManager.java中有个静态的方法,因为MysqlTest.java主动使用DriverManager.java,根据类加载机制,
导致DriverManager的静态方法loadInitialDrivers()被初始化,loadInitialDrivers()方法中ServiceLoader
loadedDrivers =
ServiceLoader.load(Driver.class);这里就回归到上面我们例子降到的SPI,接下来展开mysql-connector-java.jar如下图,在META-INF/service中有两个实现类
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
两个类都实现了java.mysql.Driver,进入com.mysql.jdbc.Driver 源码类
import java.sql.SQLException;
/**
* The Java SQL framework allows for multiple database drivers. Each driver should supply a class that implements the Driver interface
*
* <p>
* The DriverManager will try to load as many drivers as it can find and then for any given connection request, it will ask each driver in turn to try to
* connect to the target URL.
*
* <p>
* It is strongly recommended that each Driver class should be small and standalone so that the Driver class can be loaded and queried without bringing in vast
* quantities of supporting code.
*
* <p>
* When a Driver class is loaded, it should create an instance of itself and register it with the DriverManager. This means that a user can load and register a
* driver by doing Class.forName("foo.bah.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!");
}
}
/**
* Construct a new driver and register it with DriverManager
*
* @throws SQLException
* if a database error occurs.
*/
public Driver() throws SQLException {
// Required for Class.forName().newInstance()
}
}
源码可以看出com.mysql.jdbc.Driver 实现了 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!");
}
}
进入java.sql.DriverManager源码
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
/**
* Registers the given driver with the {@code DriverManager}.
* A newly-loaded driver class should call
* the method {@code registerDriver} to make itself
* known to the {@code DriverManager}. If the driver is currently
* registered, no action is taken.
*
* @param driver the new JDBC Driver that is to be registered with the
* {@code DriverManager}
* @exception SQLException if a database access error occurs
* @exception NullPointerException if {@code driver} is null
*/
public static synchronized void registerDriver(java.sql.Driver driver)
throws SQLException {
registerDriver(driver, null);
}
/**
* Registers the given driver with the {@code DriverManager}.
* A newly-loaded driver class should call
* the method {@code registerDriver} to make itself
* known to the {@code DriverManager}. If the driver is currently
* registered, no action is taken.
*
* @param driver the new JDBC Driver that is to be registered with the
* {@code DriverManager}
* @param da the {@code DriverAction} implementation to be used when
* {@code DriverManager#deregisterDriver} is called
* @exception SQLException if a database access error occurs
* @exception NullPointerException if {@code driver} is null
* @since 1.8
*/
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
源码可以看出
com.mysql.jdbc.Driver类最终被加载,并且缓存到registeredDrivers这个线程安全的ArrayList中,整个SPI过程就全部实现,那么在MysqlTest.java类中,JVM知道要找的是com.mysql.jdbc.Driver驱动,而不是其他数据库的驱动呢,看下面的代码
try{
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
通过上面的解析,可以发现,我们使用SPI查找具体的实现的时候,需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要实现。这应该也是最大的缺点,需要把所有的实现都实例化了,即便我们不需要,也都给实例化了。
说明:
对于springboot的mysql自动装配等等方式,其实大多数是触发了SPI,和MysqlTest.java中使用DriverManager.connect()是等同的.