1. Spi概述
2. 谈谈Sql驱动
3. 写一个Sql驱动
4. 通过Spi机制再写一个Sql驱动
5. 总结
-
Spi概述
Spi全称Service Provider Interface,是一种服务发现机制。通过在指定路径下(通常为:META-INF/services文件夹下)读取。读取的内容是接口实现类,将该实现类应用起来,具体使用和操作目的在本文后续会讲解。
在Spi机制中,可以看做有两方角色:微内核
+插件
。对此我们可以联想一下我们常用的开发工具Eclipse、IDEA、VScode、Atom、Sublime Text等,无一不是拥有着较大的插件生态,这些插件时如何作用起来的?
将IDE视为微内核
,以IDE运行程序为例,运行时IDE可以去回调一个接口,作为运行程序动作触发后需要进行的操作。它可以是显示控制台、统计覆盖率、打开浏览器等。这个可以提供给插件
自定义的方式,就是微内核
+插件
。微内核
拥有着一个严谨的操作,负责回调各种插件
,可替换的插件
有效降低了系统的耦合。 -
谈谈Sql驱动
这会谈Sql驱动,是因为Sql驱动也是一个插件
。我们有各种Sql驱动JDBC、ODBC...。使用它们只需要把它们引入进来,这就已经呈现出插件
可替换的特点了。
为了后面能比较顺畅的进行Sql驱动的编写,在这先提一提DriverManager
如何加载插件。
先展示下加载Sql驱动的模板语句:
public static void main(String[] args) {
String classPath = "org.jdbc.driver.MyDriver";
try {
Class.forName(classPath);
DriverManager.getConnection("url", "user", "pass");
// ...
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
}
我们看看DriverManager.getConnection()
的源码,究竟做了什么?
public static Connection getConnection(String url,
String user, String password) throws SQLException {
java.util.Properties info = new java.util.Properties();
if (user != null) {
info.put("user", user);
}
if (password != null) {
info.put("password", password);
}
return (getConnection(url, info, Reflection.getCallerClass()));
}
经过参数检验后,核心是调用了getConnection
方法,继续看看这个方法做了什么?
代码偏多,删减一些注释和语句
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
//...删
//这里可以看到:循环了成员变量registeredDrivers
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) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return (con);
}
} catch (SQLException ex) {
if (reason == null) {
reason = ex;
}
}
} else {
println(" skipping: " + aDriver.getClass().getName());
}
}
// ...删
}
让我们看看这个registeredDrivers
的定义
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
显然是个数组,那它是什么时候拥有Driver
的信息的呢?
在获取Sql连接前,有一个需要写并且好像并不起眼的代码:
Class.forName(classPath);
将Driver加载了一次。而这操作就是为了加载驱动。为什么写了这行代码就能加载驱动?
- 写一个Sql驱动
在DriverManager
中,有一个DriverManager.registerDriver
方法,用于将指定驱动加载入DriverManager.registeredDrivers
驱动列表内。Class.forName(classPath)
代码会促使驱动类内静态代码块的执行,所以只要在驱动类的静态代码块中向DriverManager
注册我们自己编写的Driver
,就可以成功引入我们的驱动。
public class MyDriver implements Driver {
private static Driver driver;
// 加载驱动
static {
try {
driver = new MyDriver();
// 向DriverManager注册驱动
DriverManager.registerDriver(driver);
} catch (SQLException e) {
e.printStackTrace();
}
}
// ... 删
}
驱动需要实现java.sql.Driver
接口,在静态代码块中注册即可实现引入驱动。至于获取连接,只需要在MyDriver.connect
中返回一个Connection
,就可以在DriverManager.getConnection
中获取到该Connection
。
然而,这样的操作虽然实现了插件化(驱动就是插件),但我们每次在获取Connection
前,都需要写一次Class.forName(classPath)
的代码来加载驱动。那么有没有办法能够不用写这行代码呢?
- 通过Spi机制再写一个Sql驱动
答案当然是有的。上述方式实现了插件化,但严格意义上不算Spi,Spi中还有一个文件夹尚未使用,这个文件夹到底有什么用处?
从上面的驱动加载可以知道,我们需要规避的是加载驱动的这个动作,那为了在用户使用时规避这个动作,就需要一个管理类来做这个操作,并且该管理类需要获取到classPath
类路径。
所以,这个文件夹META-INF/services
的意义,就是告诉读取插件的类,需要读取的插件的类路径在XXX地方(充当配置文件的意义)。
为了充分了解整个过程,我们谈谈DriverManger
中使用到的插件读取类ServiceLoader
。
在DriverManager
中,有这么一段静态代码块:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在loadInitialDrivers
内部,有这么一段代码:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
这个ServiceLoader
充当的就是插件读取类的角色。
返回的loadedDrivers
是一个迭代器,所以我们需要查看迭代器的方法:
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
// PREFIX定义: private static final String PREFIX = "META-INF/services/";
// 此处的service是ServiceLoader<Driver>中的Driver
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
}
有上述代码可知,ServiceLoader
在文件夹META-INF/services/
+接口全类名
的文件中读取插件类路径。所以利用这个Spi机制,ServiceLoader
就能在迭代器获取到类信息,并且在nextService
方法中帮助我们实现驱动类的加载。
所以对于使用Spi机制来加载驱动的插件提供者
来说,只需要在META-INF/services/java.sql.Driver
文件下,写一行插件名。
org.jdbc.driver.MyDriver
这样DriverManager
就可以通过ServiceLoader
实现的Spi机制,帮助我们加载驱动。
对于Spi机制的内核
方来说,就是需要实现Spi机制来加载插件提供者
提供的插件,加载插件后进行什么操作,就看实际业务情况了。感兴趣的小伙伴,不妨尝试自己写一个简易的DriverManager
以及ServiceLoader
,来加载自己刚写的Sql驱动,仅需要一点文件操作和反射操作即可完成。
- 总结
驱动的加载只是Spi功能的冰山一角,Spi机制的存在,能让业务代码影响较小的情况下,灵活的替换业务流程中的各种功能。目前典型的开源项目有:Dubbo
。Dubbo
框架中大量应用了Spi机制,使得开发者能够轻松替换Dubbo
中的各种功能,例如日志、rpc访问等。