spi
spi(Service Provider Interface)是一种服务发现机制。可以实现这样一种机制。由框架定义好接口,其他拓展包或者程序实现这个了这个接口后,可以通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。
例子
新建一个模块,定义接口:
HelloService
public interface HelloService {
String sayHello();
}
新建另外一个模块,实现接口:
ChineseHelloServiceImpl:
public class ChineseHelloServiceImpl implements HelloService {
public String sayHello() {
return "你好";
}
}
EnglishHelloServiceImpl:
public class EnglishHelloServiceImpl implements HelloService {
public String sayHello() {
return "Hello!";
}
}
在META-INF.services目录下新建文件夹,以接口名称命名:
文件内容为两个接口的实现:
com.rain.zhao.service.HelloService文件:
com.rain.zhao.service.impl.ChineseHelloServiceImpl
com.rain.zhao.service.impl.EnglishHelloServiceImpl
启动测试:
public class ServiceLoaderTest {
public static void main(String[] args) {
ServiceLoader<HelloService> serviceLoader = ServiceLoader.load(HelloService.class);
for (HelloService service : serviceLoader) {
System.out.println(service.sayHello());
}
}
}
控制台会打印:
你好
Hello!
原理
spi是通过serviceLoader来实现的。
ServiceLoader 实现Iterable接口,可以通过迭代器或者for语句遍历。再来看看ServiceLoader 迭代器的实现:
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
迭代器主要使用lookupIterator成员实现迭代遍历。lookupIterator 的类型是LazyIterator。LazyIterator是serviceLoader的内部类。看下LazyIterator实现:
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
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;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
public void remove() {
throw new UnsupportedOperationException();
}
}
LazyIterator通过hasNextService()方法从META-INF/services下找到对应的接口实现。再使用nextService()找到对应的类并加载。nextService()调用的Class.forName(cn, false, loader)方法,因此实际上ServiceLoader也是使用ClassLoader完成服务的加载的。
实际应用
很多框架或组件使用了spi机制。如dubbo、jdbc等。
以jdbc为例。jdk中jdbc相关实现使用了桥接模式。java.sql.DriverManager和java.sql.Driver通过组合桥接在一起,将java.sql.Driver接口交给各驱动去实现。而DriverManager提供registerDriver(java.sql.Driver)方法供各驱动注册driver实例。
以mysql驱动mysql-connector为例:
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.cj.jdbc.Driver 实现了java.sql.Driver,并通过加载类时执行静态代码完成驱动的注册。
那么com.mysql.cj.jdbc.Driver 是何时加载的呢?
在jdk 1.6之前。jdbc驱动加载需要显示调用Class.forName(“com.mysql.jdbc.Driver”)方法完成驱动类的加载:
String url = "jdbc:mysql:///url";
String user = "root";
String password = "123456";
Class.forName("com.mysql.jdbc.Driver");
Connection connection = DriverManager.getConnection(url, user, password);
在1.6之后,jdk提供了serviceLoader实现spi机制。
mysql驱动META-INF/services文件夹下通过java.sql.Driver文件注册驱动类:
DriverManager在加载过程中通过serviceLoader加载相应的jdbc驱动:
/**
* 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);
}
}
}