jdbc驱动加载机制

2021-08-06 10:56:17 浏览数 (1)

这里主要是因为最近使用jdbc连接数据库时,发现相比之前一般的连接过程,现在竟然不用加载驱动也可以了。这里研究记录下。

JDBC

JDBC是一个连接数据库的Java API,包含了相关的接口和类。

但是,他不提供针对具体数据库(MySQL、MS、Oracle)的实际操作,而只是提供了接口,以及调用框架。

和具体数据库的直接交互由对应的驱动程序完成,比如mysql的mysql-connector、oracle的ojdbc、MS的sqljdbc等。 也就是说它实际上是一种规范。目的是为了让各个数据库开发商为Java程序员提供标准的数据访问类和接口,使得独立于DBMS的Java应用程序的开发成为可能(

JDBC的组成如下:

JDBC API (统一的应用接口) JDBC Driver Manager(驱动程序管理器) JDBC 数据库驱动程序 驱动本质就是一个Java类,这个类实现了JavaAPI定义的接口

jdbc一般的连接过程

1、加载JDBC驱动程序:

代码语言:javascript复制
Class.forName("com.mysql.jdbc.Driver") ;

2、提供JDBC连接的URL

代码语言:javascript复制
String url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8

3、创建数据库的连接

代码语言:javascript复制
Connection con = DriverManager.getConnection(url , username , password ) ;

4、创建一个Statement

代码语言:javascript复制
PreparedStatement pstmt = con.prepareStatement(sql) ;

5、执行SQL语句

代码语言:javascript复制
ResultSet rs = stmt.executeQuery("SELECT * FROM ...") ;

6、处理结果

代码语言:javascript复制
while(rs.next()){
//do something
}

7、关闭JDBC对象

通过上面一般的连接步骤,我们知道,驱动的加载是由Class.forName 方法完成的。

那么Class.forName是具体怎样加载的呢?

实际上完成驱动的加载实际上是由具体的数据库驱动类的静态初始化块完成的。

这里看一下mysql的驱动类的代码:

代码语言:javascript复制
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

由于JVM对类的加载有一个逻辑是:在类被需要的时候,或者首次调用的时候就会把类加载到JVM。反过来也就是:如果类没有被需要的时候,一般是不会被加载到JVM的。

当连接数据库的时候我们调用了Class.forName语句之后,数据库驱动类被加载到JVM,那么静态初始化块就会被执行,从而完成驱动的注册工作,也就是注册到了JDBC的DriverManager类中。

由于是静态初始化块中完成的加载,所以也就不必担心驱动被加载多次,原因可以参考单例模式相关的知识。

抛弃Class.forName

在JDBC 4.0之后实际上我们不需要再调用Class.forName来加载驱动程序了,我们只需要把驱动的jar包放到工程的类加载路径里,那么驱动就会被自动加载。

这个自动加载采用的技术叫做SPI,数据库驱动厂商也都做了更新。

可以看一下jar包里面的META-INF/services目录,里面有一个java.sql.Driver的文件,文件里面包含了驱动的全路径名。

比如mysql-connector里面的内容:

代码语言:javascript复制
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
那么SPI技术又是在什么阶段加载的数据库驱动呢?

看一下JDBC的DriverManager类就知道了。

代码语言:javascript复制
public class DriverManager {
    static {
        loadInitialDrivers();//......1
        println("JDBC DriverManager initialized");
    }

    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;
           }

           AccessController.doPrivileged(new PrivilegedAction<Void>() {
                public Void run() {
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);//.....2
                   Iterator driversIterator = loadedDrivers.iterator();

上述代码片段标记…1的位置是在DriverManager类加载是执行的静态初始化块,这里会调用loadInitialDrivers方法。

再看loadInitialDrivers方法里面标记…2的位置,这里调用的 ServiceLoader.load(Driver.class); 就会加载所有在META-INF/services/java.sql.Driver文件里边的类到JVM内存,完成驱动的自动加载。 这就是SPI的优势所在,能够自动的加载类到JVM内存。这个技术在阿里的dubbo框架里面也占到了很大的分量,有兴趣的朋友可以看一下dubbo的代码,或者百度一下dubbo的扩展机制。

而且这里通过迭代器遍历了一遍就实现加载了。

跟了代码发现ServiceLoaderIterable的实现中进行了初始化,代码可以参考ServiceLoader类的nextService方法

代码语言:javascript复制
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如何区分多个驱动?

一个项目里边很可能会即连接MySQL,又连接Oracle,这样在一个工程里边就存在了多个驱动类,那么这些驱动类又是怎么区分的呢?

关键点就在于getConnection的步骤,DriverManager.getConnection中会遍历所有已经加载的驱动实例去创建连接,

当一个驱动创建连接成功时就会返回这个连接,同时不再调用其他的驱动实例。

DriverManager关键代码如下:

代码语言:javascript复制
private static Connection getConnection(
       String url, java.util.Properties info, Class<?> caller) throws SQLException {
       /*
        * When callerCl is null, we should check the application's
        * (which is invoking this class indirectly)
        * classloader, so that the JDBC driver class outside rt.jar
        * can be loaded from here.
        */
       ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;【调用方的类所在类加载器】
       synchronized(DriverManager.class) {
           // synchronize loading of the correct classloader.
           if (callerCL == null) {
               callerCL = Thread.currentThread().getContextClassLoader();
           }
       }
 
       if(url == null) {
           throw new SQLException("The url cannot be null", "08001");
       }
 
       println("DriverManager.getConnection(""   url   "")");
 
       // Walk through the loaded registeredDrivers attempting to make a connection.
       // Remember the first exception that gets raised so we can reraise it.
       SQLException reason = null;
 
       for(DriverInfo aDriver : registeredDrivers) {
           // If the caller does not have permission to load the driver then
           // skip it.
           if(isDriverAllowed(aDriver.driver, callerCL)) {(JDBC注册原理与自定义类加载器解决com.cloudera.hive.jdbc41.HS2Driver的加载【重点】中用一个代理中间人骗过了)
               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");
   }

是不是每个驱动实例都真真实实的要尝试建立连接呢?不是的!

每个驱动实例在getConnetion的第一步就是按照url判断是不是符合自己的处理规则,是的话才会和db建立连接。

比如,MySQL驱动类中的关键代码:

代码语言:javascript复制
public static Driver getDriver(String paramString) throws SQLException {
    
        //省略部分代码。。。。
        Iterator localIterator = registeredDrivers.iterator();
        //遍历注册的驱动
        while (localIterator.hasNext()) {
            DriverInfo localDriverInfo = (DriverInfo) localIterator.next();
            if (isDriverAllowed(localDriverInfo.driver, localClass))
                try {
                    //如果accepsURL() 为true,返回对应的driver
                    if (localDriverInfo.driver.acceptsURL(paramString)) {
                        //返回对应的driver
                        return localDriverInfo.driver;
                    }
                } catch (SQLException localSQLException) {
                }
            else
                println("    skipping: "  localDriverInfo.driver.getClass().getName());
        }
        throw new SQLException("No suitable driver", "08001");
        //-----省略部分代码
    }
代码语言:javascript复制
    public boolean acceptsURL(String url) throws SQLException {
        return (parseURL(url, null) != null);
    }

    public Properties parseURL(String url, Properties defaults)
            throws java.sql.SQLException {
        Properties urlProps = (defaults != null) ? new Properties(defaults)
                : new Properties();

        if (url == null) {
            return null;
        }

        if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX)
                && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX)
                && !StringUtils.startsWithIgnoreCase(url,
                        LOADBALANCE_URL_PREFIX)
                && !StringUtils.startsWithIgnoreCase(url,
                        REPLICATION_URL_PREFIX)) { //$NON-NLS-1$

            return null;
        }
        //......

为什么JDBC驱动没有加载

而我省略Class.forName加载驱动后,遇到一个问题,jdbc连接在我本地可以正常运行,但是线上环境会报 No suitable driver found for jdbc

而如果代码中通过Class.forName声明,却不会报错,可以肯定是通过SPI注册的时候有问题。

目前猜测:

1.跟运行环境有关,本地和显示安装的jdk版本不同,显示的jdk的driverManager还没有实现SPI自动加载机制。

2.线上环境在使用SPI自动加载去加载驱动时,加载某个驱动报错,导致后续加载驱动不再执行。

代码语言:javascript复制
/* 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
}

注释中也写到可能会有驱动类不存在的情况,所以加了一个异常处理。

总结

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

参考:

https://segmentfault.com/a/1190000021287439

https://blog.csdn.net/buqutianya/article/details/78936947

https://www.cnblogs.com/silyvin/p/12192617.html

https://blog.csdn.net/zhouym_/article/details/90580197

0 人点赞