JAVA的SPI机制-案例-JDBC
建议打开Idea,引入mysql的驱动包,跟一遍代码
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency>
原生JDBC
JDBC提供了java访问数据库的规范,当连接mysql时,引入mysql的jdbc驱动包;连接sqlserver时,引入sqlserver的jdbc驱动包;oracle也是一样。各种驱动像是一个部件,想用哪个直接更换到对应的驱动即可,代码里面连接数据库的操作不用做任何改动。
JDBC是怎么做到的?
回想一下jdbc的原生写法:
加载驱动
Class.forName(classname);
建立连接
Connection conn = DriverManager.getConnection(url);
在JDBC4.0之后,不需要加载驱动,也可以成功获取到连接,那么这个时候驱动是如何被加载到JVM并使用的?
DriverManager.getConnection
点击进入DriverManager.getConnection(url)方法
public static Connection getConnection(String url)
throws SQLException {
java.util.Properties info = new java.util.Properties();
return (getConnection(url, info, Reflection.getCallerClass()));
}
逻辑在getConnection方法中,继续点击
// Worker method called by the public getConnection() methods.
private static Connection getConnection(
String url, java.util.Properties info, Class<?> caller) throws SQLException {
// 省略...
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
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());
}
}
// if we got here nobody could connect.
if (reason != null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
这里会遍历registeredDrivers进行检查并获取到真实的连接,跟踪看到声明,是DriverManager的私有成员变量,默认初始化为空List:
private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();
那么registeredDrivers这个集合的内容又来自于哪里? 在DriverManager中,只有一个registerDriver的方法会往里面设值
public static synchronized void registerDriver(java.sql.Driver driver,
DriverAction da)
throws SQLException {
/* Register the driver if it has not already been added to our list */
if(driver != null) {
registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
} else {
// This is for compatibility with the original DriverManager
throw new NullPointerException();
}
println("registerDriver: " + driver);
}
但是在DriverManager中并没有看到调用的地方,说明这个方法是被外部调用的。引入mysql的驱动,然后再看java.sql.DriverManager#registerDriver(java.sql.Driver)的引用,发现
mysql驱动包的静态代码块
点击进入到com.mysql.fabric.jdbc.FabricMySQLDriver类,发现调用是在静态代码块中
// Register ourselves with the DriverManager
static {
try {
DriverManager.registerDriver(new FabricMySQLDriver());
} catch (SQLException ex) {
throw new RuntimeException("Can't register driver", ex);
}
}
嗯。。静态代码块?需要类被加载的时候才执行,但是我们不调用Class.forName,怎么才能加载FabricMySQLDriver呢?但是事实证明肯定被加载了,因为我们可以正常连接到数据库。
回到DriverManager的静态代码块
不慌,继续回到JDK提供的DriverManager中,发现里面也有一个静态代码块,其实这里的注释已经说明了一切。
/**
* 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");
}
进入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;
}
// 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() {
// Java的SPI机制加载classpath中所有的Driver实现类
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(); // 重点是这个next里面
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
可以看到有一行ServiceLoader.load(Driver.class),ServiceLoader的全路径是java.util.ServiceLoader,来自于jdk。这就是Java的SPI机制。
可以看到,在mysql的驱动包下面,有一个META-INF/services/java.sql.Driver的文件
文件内容为
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
了解SPI机制原理请参考:JAVA的SPI机制
这里的load方法会把classpath中的所有Driver的实现类加载进来并注册到DriverManager里面,作为Driver的实现类,com.mysql.fabric.jdbc.FabricMySQLDriver自然也会被加载进来,其中的静态代码块也就执行了,将自己注册到DriverManger中,供DriverManger.getConnection()进行使用。
画个图
最后画个图总结一下
画了半天,将就看,领会精神。。
DriverManager在加载时通过静态代码块,已经通过SPI机制,通过ServiceLoader将classpath的实现类加载进来,并通过实现类的静态代码块完成真实driver的注册,以便于在调用者调用getConnection的时候可以遍历已有的driver进行连接的获取。
最后,可能稍微细心点会发现getConnection内部的for循环中有一个检测isDriverAllowed(aDriver.driver, callerCL),具体方法体
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
boolean result = false;
if(driver != null) {
Class<?> aClass = null;
try {
aClass = Class.forName(driver.getClass().getName(), true, classLoader);
} catch (Exception ex) {
result = false;
}
result = ( aClass == driver.getClass() ) ? true : false; // 这个三目运算...
}
return result;
}
这个方法的主要作用是为了确保加载到的driver class与调用者所在的类加载器是同一个,for里面的注释也进行了说明
// If the caller does not have permission to load the driver then
// skip it.
疑问
所有,如果我的环境里面同事存在多个驱动包,比如mysql与sqlsever的驱动包,这里会随机返回一个?