JDBC驱动加载机制

Posted 不去天涯

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDBC驱动加载机制相关的知识,希望对你有一定的参考价值。

JDBC

说道JDBC我们写Java的程序员实在是太过熟悉了,如今的后端系统不论大小几乎都抹不开和数据库存在联系。

JDBC是一个连接数据库的Java API,包含了相关的接口和类。但是,他不提供针对具体数据库(mysql、MS、Oracle)的实际操作,而只是提供了接口,以及调用框架。和具体数据库的直接交互由对应的驱动程序完成,比如mysql的mysql-connector、oracle的ojdbc、MS的sqljdbc等。

现在ORM框架火热流行,程序员需要关注的数据库交互更加少了,需要做的基本上就是配框架,写sql就好了。

所以,让我们先回顾一下jdbc的一般连接过程。

jdbc连接过程

1、加载JDBC驱动程序:

Class.forName("com.mysql.jdbc.Driver") ;   

2、提供JDBC连接的URL

String url = jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8

3、创建数据库的连接

    Connection con =    
             DriverManager.getConnection(url , username , password ) ;   

4、创建一个Statement

PreparedStatement pstmt = con.prepareStatement(sql) ;   

5、执行SQL语句

ResultSet rs = stmt.executeQuery("SELECT * FROM ...") ;   

6、处理结果

     while(rs.next())   
         //do something
       

7、关闭JDBC对象

Class.forName作用

我们都知道,也听了无数遍,驱动的加载是由Class.forName 方法完成的。

但是,让我们深究一下,Class.forName是JSE里面加载一个类到JVM内存的方法,为什么又会关联了JDBC的驱动加载逻辑呢?

如果是的话,那么其他和JDBC无关的类加载也会调用JDBC驱动加载的相关逻辑吗?

是不是觉得问题很大,是不是觉得如果是我们来说设计JSE也不会做这样的事情?

给Class.forName正名

确实JDBC驱动的加载是在Class.forName这一步完成的,但是完成这个工作的是加载的具体的数据库驱动类的静态初始化块完成的。

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

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里面的内容:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver

那么SPI技术又是在什么阶段加载的数据库驱动呢?看一下JDBC的DriverManager类就知道了。

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的扩展机制。

JDBC如何区分多个驱动?

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

关键点就在于getConnection的步骤,DriverManager.getConnection中会遍历所有已经加载的驱动实例去创建连接,当一个驱动创建连接成功时就会返回这个连接,同时不再调用其他的驱动实例。DriverManager关键代码如下:

private static Connection getConnection(
    //.....

    for(DriverInfo aDriver : registeredDrivers) 
        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());
        
     

    //......

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

每个驱动实例在getConnetion的第一步就是按照url判断是不是符合自己的处理规则,是的话才会和db建立连接。比如,MySQL驱动类中的关键代码:

    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驱动加载机制的主要内容,如果未能解决你的问题,请参考以下文章

jvm原理(24)通过JDBC驱动加载深刻理解线程上下文类加载器机制

JDBC SPI加载机制

大数据平台底层技术-JAVA篇-如何动态加载不同版本的 HIVE JDBC 驱动 - 一文读懂JAVA的类加载机制...

Java SPI 机制及其实现

JDBC 加载mysql数据库驱动

jdbc连接一些问题和常规操作