Java SPI的具体约定为:
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader
应用:common-logging、jdbc4
用JDBC做例子:
JDBC4之前,需要先Class.forName("com.mysql.jdbc.Driver");加载驱动
驱动的加载过程,实际上是通过反射加载com.mysql.jdbc.Driver类,在类静态块中注册驱动:
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
注册过程就是在DriverManager的CopyOnWriteArrayList<DriverInfo>列表中添加一个DriverInfo实例
在调用驱动获取连接(Connection conn=DriverManager.getConnection(url,user,password);)时,实质是遍历这个CopyOnWriteArrayList,尝试用所有已注册的驱动创建连接,并返回第一个非null的连接:
for (DriverInfo aDriver : registeredDrivers) {
if (isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
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());
}
}
JDBC4之后:
不需要再显式通过Class.forName注册驱动,原因:
DriverManager在静态块通过SPI完成了注册过程:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
……
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
while(driversIterator.hasNext()) {
driversIterator.next();
}
……
}
……
}
}
load方法实际上是调用当前线程类加载器或根加载器加载java.sql.Driver.class创建了一个ServiceLoader实例
在ServiceLoader构造方法中,又通过reload方法创建了一个内部实现的懒加载迭代器LazyIterator的实例
之后通过loadedDrivers.iterator()获取该实例,在遍历方法next()中,完成com.mysql.jdbc.Driver类的加载:
next()方法调用了nextService()方法:
private S nextService() {
……
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
……
}
之所以是限定在/META-INF/services目录,是因为LazyIterator的hasNextServices方法在每个Name前面都添加了这个前缀:
String fullName = PREFIX + service.getName();
自定义实现时可以修改,如Dubbo框架就限定在 /META-INF/dubbo/internal、/META-INF/dubbo和/META-INF/services目录下:
类名:ExtensionLoader
private Map<String, Class<?>> loadExtensionClasses() {
……
this.loadFile(extensionClasses, "META-INF/dubbo/internal/");
this.loadFile(extensionClasses, "META-INF/dubbo/");
this.loadFile(extensionClasses, "META-INF/services/");
return extensionClasses;
}
测试:
在 try-catch 块后面一句打断点,debug运行:
查看mysql-connector的META-INF/services/:
之所以加载了第一个而不是第二个,是因为测试的时候,使用的url是 jdbc:mysql:// 开头,com.mysql.jdbc.Driver已经可以返回一个非空连接。
即使会尝试让所有驱动都连接一遍:
在FabricMySQLDriver.class的connect方法,可以看到:
return !url.startsWith("jdbc:mysql:fabric://") ? null : super.parseURL(url.replaceAll("fabric:", ""), defaults);
即:测试用的url会让FabricMySQLDriver创建一个null连接,从而被getConnection方法pass掉