JDBC驱动加载机制

JDBC

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

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

如今ORM框架火热流行,程序员须要关注的数据库交互更加少了,须要作的基本上就是配框架,写sql就行了。程序员

因此,让咱们先回顾一下jdbc的通常链接过程。web

jdbc链接过程

一、加载JDBC驱动程序: sql

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

二、提供JDBC链接的URL 数据库

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

三、建立数据库的链接 后端

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

四、建立一个Statement oracle

PreparedStatement pstmt = con.prepareStatement(sql) ;

五、执行SQL语句 框架

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

六、处理结果 svg

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

七、关闭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;
        }
        //......