重点是思想
????????SPI只是一种思想,不是设计模式,是一种服务发现机制,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。?SPI的作用就是为这些被扩展的API寻找服务实现。
????????API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。?从使用人员上来说,API 直接被应用开发人员使用。
????????SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。? 从使用人员上来说,SPI 被框架扩展人员使用。
熟悉JDBC的都知道,在 jdbc 4.0之前,在使用 DriverManager 获取 DB 连接之前,我们总是需要显示的实例化 DB 驱动。比如,对 mysql,典型的代码如下:
Connection conn = null;
PreparedStatement stmt = null;
try{
// 注册 JDBC driver, 加载数据库驱动class
Class.forName("com.mysql.jdbc.Driver");
// 获取连接
conn = DriverManagger.getConnection(DB_URL,USER,PASSWD);
// 执行一条sql
stmt = conn.prepareStatement();
String sql = "select * from t_user";
ResultSet rs = stmt.executeQuery(sql);
// 数据解包
while(ts.next()){
// 根据列名获取列值
// ...
} catch(SQLException se) {
// ...
} final {
try {
if (stmt!=null) stmt.close();
} catch(Exception e) {/*ignored*/}
try {
if (conn!=null) conn.close();
} catch(Exception e) {/*ignored*/}
}
}
????????JDBC的开始总是需要通过 Class.forName 显式加载驱动,否则将找不到对应的DB驱动。但是 JDBC4.0 开始,这个显式的初始化不再是必选项了,它存在的意义只是为了向上兼容。
????????那么JDBC4.0之后,我们的应用是如何找到对应的驱动呢?
????????答案就是 SPI(Service Provider Interface)。Java在语言层面为我们提供了一种方便地创建可扩展应用的途径。SPI提供了一种JVM级别的服务发现机制,我们只需要按照SPI的要求,在jar包中进行适当的配置,jvm就会在运行时通过懒加载,帮我们找到所需的服务并加载。如果我们一直不使用某个服务,那么它不会被加载,一定程度上避免了资源的浪费。
通过一个ServiceLoader.load(xx.class)简单的例子看看如何最小化构建一个基于SPI的服务。
????????Java SPI 并没有强制必须使用 interface 或 abstract class,完全可以将 class 注册为 SPI 注册服务,但是作为可扩展服务,使用 interface 或 abstract class 是一个好习惯。
????????在包 “cn.spi”中定义一个接口Animal:
package cn.spi;
public interface Animal {
void eat();
void sleep();
}
package cn.spi.impl;
import cn.spi.Animal;
public class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
@Override
public void sleep() {
System.out.println("Dog is sleeping");
}
}
在main目录下创建目录 "resources/META-INF/services"
mkdir -p resources/META-INF/services
再在该目录下创建以接口Animal全限定名为名(cn.spi.Animal)的配置文件,文件内容为该接口的实现类的全限定名,即
"cn.spi.impl.Dog"
完成此步骤后,在当前maven项目的 src/main/resources/META-INF/services下有这么一个配置文件:"cn.spi.Animal",并且它的内容为"cn.spi.impl.Dog"。
注意本步骤的要点:
package cn.spi
import org.junit.Test;
import java.util.ServiceLoader;
public class AnimalTest {
@Test
public void animalTest() {
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
for(Animal animal: animals) {
animal.eat();
animal.sleep();
}
}
}
执行结果:
Dog is eating
Dog is sleeping
可见,虽然我们没有显式使用Animal的实现类Dog,但是java帮我们自动加载了该实现类。
接下来从代码层面看看SPI都为我们做了什么。首先看看 java.util.ServiceLoader 的实现。我们看到 ServiceLoader 使用非常简单,只需要调用一个静态方法 load 并以要加载的服务的父类(通常是一个interface或abstract class)作为参数,jvm就会帮我们构建好当前进程中所有注册到 META-INF/services/[service full qualified class name] 的服务。
//自动从META-INF/services/[service full qualified class name] 构建
ServiceLoader<Animal> animals = ServiceLoader.load(Animal.class);
????????下面是构造ServiceLoader实例的相关代码。ServiceLoader必须通过静态方法load(Class service)的方式加载服务,默认会使用当前线程的上下文class loader对象。构造完ServiceLoader后,ServiceLoader实例并不会立刻扫描当前进程中的服务实例,而是创建一个LazyIterator懒加载迭代器,在实际使用时再扫描所有jar包找到对应的服务。懒加载迭代器被保存在一个内部成员lookupIterator中。
public final class ServiceLoader<S> implements Iterable<S>{
// 保存懒加载迭代器
private LazyIterator lookupIterator;
// 通过service的class创建ServiceLoader实例,默认使用上下文classloader
public static <S> ServiceLoader<S> load(Class<S> service) {
// 从线程上下文中获取类加载器
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
// 构建ServiceLoader实例 service:接口class, loader:类加载器
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
return new ServiceLoader<>(service, loader);
}
// ServiceLoader 构造函数,私有类型,必须通过ServiceLoader.load(Class<?>)静态方法来创建ServiceLoader实例
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// 重新load指定serivice的实现。通过LazyIterator实现懒加载。
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
...
}
????????在调用了ServiceLoader animals = ServiceLoader.load(Animal.class)之后,ServiceLoader会返回一个Animal.class类型的迭代器,但此时在ServiceLoader内部只是创建了一个 LazyIterator,而不会真正通过classloader在classpath中寻找相关的服务实现。
????????
public final class ServiceLoader<S> implements Iterable<S>
{
...
// 缓存的service provider,按照初始化顺序排列。
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 当前的LazyIterator迭代器指针,服务懒加载迭代器
private LazyIterator lookupIterator;
...
// 创建ServiceLoader迭代器,隐藏了LazyIterator的实现细节
public Iterator<S> iterator() {
return new Iterator<S>() {
// 创建Iterator迭代器时的ServiceLoader.providers快照,
// 因此在首次迭代时,iterator 总是会通过LazyIterator进行懒加载
Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
public boolean hasNext() {
// 如果已经扫描过,则对providers进行迭代;
if (knownProviders.hasNext())
return true;
// 如果没有扫描过,则通过lookupIterator进行扫描和懒加载
return lookupIterator.hasNext();
}
public S next() {
// 如果已经扫描过,则对providers进行迭代;
if (knownProviders.hasNext())
return knownProviders.next().getValue();
// 如果没有扫描过,则通过lookupIterator进行扫描和懒加载
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
...
}
ServiceLoader的迭代器很简单:
懒加载迭代器LazyIterator主要实现以下功能:
下面是LazyIterator的源码及注释:
public final class ServiceLoader<S> implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
...
// 懒加载迭代器实现
private class LazyIterator implements Iterator<S>{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
// 当前service配置文件的内容迭代器
// 即对services进行遍历,取出一个services配置文件,再对该文件按行解析,每行代表一个具体的service实现类,pending是某个services配置文件中service实现类的迭代器
Iterator<String> pending = null;
String nextName = null;
// 构造懒加载迭代器
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 首次迭代时,configs为空,尝试通过classloader获取名为:
// "META-INF/services/[服务全限定名]"的所有配置文件
if (configs == null) {
try {
// 注意fullName的定义:"META-INF/services/[服务全限定名]"
String fullName = PREFIX + service.getName();
// 通过ClassLoader.getResources()获得资源URL集合
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 如果pending为空,或者pending已经迭代到迭代器末尾,则尝试解析下一个services配置文件
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// 对当前pending内容进行遍历,每一项代表services的一个实现类
nextName = pending.next();
return true;
}
}
...
}
最后,附上parse及parseLine的代码,可以发现,parseLine中会对服务实现类进行去重,所以在一个或多个services配置文件中配置多次的服务实现类只会被处理一次。
public final class ServiceLoader<S> implements Iterable<S>{
...
// 按行解析给定配置文件。如果解析出的服务实现类没有被其他已解析的配置文件配置过,则通过参数nams返回给parse方法
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc, List<String> names) throws IOException, ServiceConfigurationError {
String ln = r.readLine();
if (ln == null) {
return -1;
}
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 去重,防止重复配置服务,每个服务实现类只会被解析一次
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
// 解析指定的作为SPI配置文件的URL的内容
private Iterator<String> parse(Class<?> service, URL u) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
...
}
最后,以JDBC为例,看一个SPI的实际使用场景。在文章开始,我们提到过,JDBC4.0之前,我们总是需要在业务代码中显式地实例化DB驱动实现类:
Class.forName("com.mysql.jdbc.Driver");
为什么JDBC4.0之后不需要了呢?
答案就在下面的代码中。在系统启动时,DriverManager 静态初始化时会通过 ServiceLoader 对所有jar包中被注册为 java.sql.Driver 服务的驱动实现类进行初始化,这样就避免了上面通过Class.forName手动初始化的繁琐工作。DriverManager.getConnection()方法会触发DriverManager的初始化。
public class DriverManager {
// JDBC驱动注册中心,所有加载的JDBC驱动都注册在该CopyOnWriteArrayList中
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
...
/* Prevent the DriverManager class from being instantiated. */
private 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() {
// 如果通过jdbc.drivers配置了驱动,则在本方法最后进行实例化
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加载所有通过SPI方式注册的"java.sql.Driver"服务
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
// 遍历ServiceLoader实例进行强制实例化,因此除了遍历不做任何其他操作
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
}
);
println("DriverManager.initialize: jdbc.drivers = " + drivers);
// 强制加载"jdbc.driver"环境变量中配置的DB驱动
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);
}
}
}
...
}
以mySql驱动为例看看驱动实例化时做了什么:
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
//
// Register ourselves with the DriverManager
//
static {
try {
// 向 DriverManager 注册自己
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()
}
}
再看看mysql驱动jar包中对service的配置:
因此,只要某个驱动以这种方式被引用并被上下文class loader加载,那么该驱动就会通过SPI的方式被自动发现和加载。实际使用时,Driver.getDriver(url)会通过DB连接url获取到正确的驱动并建立与DB的连接。