前言
最近在看朱政科的《HikariCP数据库连接池实战》,书中关于JDBC部分的讲解提到了JDK SPI,所以笔者就决定好好研究一下这方面的知识,并通过自己的理解输出一篇文章。如果有描述有误或者理解偏差的地方,欢迎各位大佬指正,谢谢!
JDK SPI是什么
维基百科给的定义:Service Provider Interface (SPI) is an API intended to be implemented or extended by a third party. It can be used to enable framework extension and replaceable components。
翻译一下可知,SPI是**Service Provider Interface(服务提供者接口)**的简称,它是一个可以由第三方实现和扩展的API。
定义可能看起来不太直观,看完下面一个小节关于JDK SPI的使用 ,相信您对JDK SPI会有一个明确的理解。
JDK SPI的简单使用
-
首先定义一个
Fruit接口public interface Fruit { String getName(); } -
分别定义两个实现类
public class Apple implements Fruit { @Override public String getName() { return "apple"; } } public class Orange implements Fruit { @Override public String getName() { return "orange"; } } -
在classpath下面创建
META-INF/services文件夹,并在文件夹中创建一个文件cn.ajin.practical.java.spi.Fruit,并在文件中声明具体的实现类cn.ajin.practical.java.spi.Orange -
编写测试代码查看输出结果
ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class); Iterator<Fruit> iterator = serviceLoader.iterator(); while (iterator.hasNext()) { Fruit fruit = iterator.next(); System.out.println(fruit.getName()); }控制台输出结果:orange
我们看到控制台输出的是我们定义在
cn.ajin.practical.java.spi.Fruit文件中的Orange。
JDK SPI原理探究
通过JDK SPI机制,我们可以创建出一个可扩展的程序,只要接口定义好,再定义好接口的实现类,我们的应用程序需要哪些 实现类的时候,通过上面的步骤3定义好就可以了,而原有程序逻辑无需修改。
类比一下Spring Web应用,通常定义好一个Service接口,然后编写Service实现类的逻辑,然后在实现类上标上注解@Componet就可以了,Controller类中只要注入Service接口即可 获取Service实现类了。
其实这是一种面向接口的编程,我们的应用程序无需关注实现类,但是最终却可以获取到我们需要的实现类。
再说我们比较熟悉的Spring Boot,自动装配流程需要获取自动装配类,这个过程是通过一个工具类去查找所有jar下面的spring.factories文件,获取文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的自动装配类(如下所示)来实现的。这和JDK SPI机制的编程模式是不是有一点点相似呢?
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
JDK SPI机制是一种服务发现机制,动态地为接口寻找服务实现。它的核心来自于ServiceLoader这个类。
ServiceLoader
这里再列出测试代码:
ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class);
Iterator<Fruit> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
Fruit fruit = iterator.next();
System.out.println(fruit.getName());
}
在阅读下面的源码分析部分,读者可以参考上图。
ServiceLoader#load(java.lang.Class<S>)
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前线程的ClassLoader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
ServiceLoader.load(service, cl);最终会调用下面的构造器
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();
}
reload()方法需要关注一下
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
这里创建了
LazyIterator,后面会使用它来遍历Fruit.class。
总结一下:
ServiceLoader#load(java.lang.Class<S>)ServiceLoader.load(service, cl)、ServiceLoader#reloadnew LazyIterator
可见,
ServiceLoader#load(Class<S>)实际上是创建了一个LazyIterator迭代器对象。
serviceLoader.iterator()
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();
}
};
}
这里创建了一个
Iterator,当我们调用它的hasNext方法时,debug走下来看,其实就是调用LazyIterator#hasNext方法,然后会调用hasNextService方法。
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);
}
}
不忙往下看,这里先总结一下,通过
serviceLoader.iterator()方法创建的Iterator对象,它的hasNext方法和next方法实际上是调用了LazyIterator中的对应方法,所以真正的主角就是LazyIterator对象。
LazyIterator#hasNextService
private boolean hasNextService() {
if (nextName != null) {
return true;
}
// 如果不是第一次执行,configs !=null ,不仅会进入第一个判断
if (configs == null) {
try {
// 文件名:META-INF/services/cn.ajin.practical.java.spi.Fruit
// service.getName() : 类名,这里就是 cn.ajin.practical.java.spi.Fruit
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
// 根据fullName获取Enumeration对象
configs = loader.getResources(fullName);
} catch (IOException x) {
...
}
}
while ((pending == null) || !pending.hasNext()) {
// 判断元素是否存在
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
// cn.ajin.practical.java.spi.Orange
nextName = pending.next();
return true;
}
-
获取
fullName(其实就是我们在META-INF/services目录下定义的文件名) -
返回
CompoundEnumeration -
判断
CompoundEnumeration中是否有元素-
如果不存在元素,直接返回false
-
反之,则去解析出下一个元素(如果是第一次执行,则会去解析第一个元素)
pending = parse(service, configs.nextElement()); -
将
pending中的第一个元素赋值给nextName引用变量
-
下面是LazyIterator中比较重要的实例变量:
// 根据fullName查找到的 Enumeration集合
Enumeration<URL> configs = null;
// 通过config解析出的迭代器
Iterator<String> pending = null;
// 下一个元素
String nextName = null;
总结LazyIterator迭代原理
原理总结
到这里,我想大家对于ServiceLoader加载定义好的实现类的原理应该有比较清晰的了解吧,其实就是先获取文件名,并根据文件名获取一个Enumeration对象,再去迭代,最终创建实现类对象。
JDBC与SPI
接下来,我们再来看看JDBC中的SPI机制。
原生JDBC的使用
Class.forName("com.mysql.jdbc.Driver");
Connection connection = null;
Statement statement = null;
ResultSet rs = null;
try {
connection = DriverManager.getConnection(url, userName, password);
statement = connection.createStatement();
rs = statement.executeQuery("select id,name,age from s_user");
while (rs.next()) {
int id = rs.getInt("id");
String name = rs.getString("name");
int age = rs.getInt("age");
}
} finally {
if (rs != null) {
rs.close();
}
if (statement != null) {
statement.close();
}
if (connection != null) {
connection.close();
}
}
原生JDBC的写法需要Class.forName方法来指定驱动类,但是这样显然是很不灵活的,今天是mysql,明天是oracle,这样的写法无法做到无缝切换数据库驱动。通过JDBC的SPI机制,可以很轻松的对接不同数据库厂商的JDBC驱动。
JDBC SPI机制
DriverManager#getConnection(url, userName, password)方法的首次调用会触发静态代码块的执行。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
DriverManager#loadInitialDrivers
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;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 使用SPI机制加载Driver驱动类
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
// 反射加载驱动类
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
...
}
driversIterator.next()方法会触发驱动类的static代码块的执行
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
// 将Driver注册到DriverManager中去
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
JDK SPI机制的优缺点
- 优点:JDK SPI使得我们可以面向接口编程,无需硬编码的方式即可引入实现类
- 缺点:
- 不能按需加载,虽然
ServiceLoader做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。 - 多线程并发场景下加载是线程不安全的
- 不能按需加载,虽然
鉴于SPI的这些缺点,Dubbo SPI做了一些优化,读者如果感兴趣,可以了解一下.
全文总结
本文首先介绍了JDK SPI的基本概念,并通过一个Demo让读者了解SPI的使用,然后探索了SPI机制的底层原理,接着引申到JDBC中的SPI使用,最后还稍微探索了SPI的优缺点,算是一个比较完整的讲述吧。
使用SPI机制,我们可以很灵活地引入接口的实现类,同时SPI也存在一定的缺陷。笔者对Dubbo的底层原理可以说不太清楚,所以就没有去介绍Dubbo中的SPI,后面如果学习到Dubbo这块的内容,应该会补上一篇文章来讲述,读者也可以自己去探索学习。谢谢大家的阅读,周末愉快!