转载

JDBC驱动为什么没加载

使用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<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;
    }
});

第一次看的时候很疑惑,为什么只通过迭代器遍历了一遍就实现加载了。

跟了代码发现 ServiceLoaderIterable 的实现中进行了初始化,代码可以参考 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 下,引入之后就没有问题了。

总结

  1. 使用Class.forName加载驱动时,把类加载到内存同时进行了初始化,注册驱动的过程发生在初始化中。
  2. JDBC4.0后可以通过SPI方式注册驱动。
  3. 通过SPI方式注册驱动时如果有一个驱动加载出问题,会影响后续的驱动加载。
原文  https://segmentfault.com/a/1190000021287439
正文到此结束
Loading...