使用Class.forName加载驱动
使用JDBC连接数据库的时候,需要先加载驱动。可以通过Class.forName声明要加载的驱动,加载这个词在这里其实不太明确,因为Class.forName不只是把类加载到了内存中,还会初始化(static块中的代码会被执行)。注册驱动其实就发生在 static 块中。比如mysql的驱动com.mysql.cj.jdbc.Driver
static {
try {
java.sql.DriverManager.registerDriver(new Driver());
} catch (SQLException E) {
throw new RuntimeException("Can't register driver!");
}
}
所以这里是无法使用ClassLoader.loadClass()
来替换的。
不使用Class.forName
在JDBC 4.0之后,可以通过SPI的方式加载驱动。
在驱动相应的jar包里,META-INF/services目录下,会有名为java.sql.Driver的文件,里面的内容是驱动的全路径名。
比如在mysql-connector-java-8.0.16.jar
中,META-INF/services目录下的java.sql.Driver内容为:
com.mysql.cj.jdbc.Driver
DriverManager初始化的时候会通过SPI加载所有Driver接口的实现类
在DriverManager中有如下代码
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
loadInitialDrivers
方法中包含了两部分
- 通过系统属性jdbc.drivers加载驱动
- 通过SPI的方式加载
看一下通过SPI方式加载的部分
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator 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();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
第一次看的时候很疑惑,为什么只通过迭代器遍历了一遍就实现加载了。
跟了代码发现ServiceLoader
在Iterable
的实现中进行了初始化,代码可以参考ServiceLoader
类的nextService
方法
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
}
注意第一次调用Class.forName(cn, false, loader)
并没有初始化,而是在后面service.cast(c.newInstance())
进行的初始化。
为什么JDBC驱动没有加载
最近碰到了个问题,使用phoenix进行jdbc连接的时候报错
java.sql.SQLException: No suitable driver found for jdbc:phoenix:127.0.0.1:2182
而如果代码中通过Class.forName
声明,却不会报错,可以肯定是通过SPI注册的时候有问题。
phoenix-core.jar包中的java.sql.Driver内容为
org.apache.phoenix.jdbc.PhoenixDriver
和我使用Class.forName
声明时是一样的
后来在跟代码的时候发现通过SPI加载驱动时,获取到了一个驱动org.apache.calcite.avatica.remote.Driver
,而在加载这个类的时候报错了,classpath中并没有这个类。参考代码,可以看到遍历的时候只要有一次报错后续就不会执行了。
/* 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();
}
} catch(Throwable t) {
// Do nothing
}
注释中也写到可能会有驱动类不存在的情况,所以加了一个异常处理。
看到org.apache.calcite.avatica.remote.Driver
类,想到了项目中使用的kylin,翻看kylin-jdbc
相应的java.sql.Driver内容为
org.apache.calcite.avatica.remote.Driver
而org.apache.calcite.avatica.remote.Driver
这个类其实是在org.apache.calcite.avatica:avatica
下,引入之后就没有问题了。
总结
- 使用Class.forName加载驱动时,把类加载到内存同时进行了初始化,注册驱动的过程发生在初始化中。
- JDBC4.0后可以通过SPI方式注册驱动。
- 通过SPI方式注册驱动时如果有一个驱动加载出问题,会影响后续的驱动加载。