使用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()
来替换的。
在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
方法中包含了两部分
看一下通过SPI方式加载的部分
AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { 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(); } } 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())
进行的初始化。
最近碰到了个问题,使用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
下,引入之后就没有问题了。