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!");
}
}
【Java SPI & JDBC相关源码解析】注册过程就是在DriverManager的CopyOnWriteArrayList
在调用驱动获取连接(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() {
public Void run() {
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator 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> 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掉
推荐阅读
- Java|Java基础——数组
- 人工智能|干货!人体姿态估计与运动预测
- java简介|Java是什么(Java能用来干什么?)
- Java|规范的打印日志
- Linux|109 个实用 shell 脚本
- 程序员|【高级Java架构师系统学习】毕业一年萌新的Java大厂面经,最新整理
- Spring注解驱动第十讲--@Autowired使用
- SqlServer|sql server的UPDLOCK、HOLDLOCK试验
- jvm|【JVM】JVM08(java内存模型解析[JMM])
- 技术|为参加2021年蓝桥杯Java软件开发大学B组细心整理常见基础知识、搜索和常用算法解析例题(持续更新...)