你真的的懂JDBC?

一、前言

Java中操作数据库元老是使用JDBC,而JDBC内部是如何实现的,为何每次使用时候都是写那些不理解的几行固定代码?这些看似不相关的代码内部是否有瓜葛那,下面进来探讨一二。

二、一个例子

public class TestJdbc {

    public static final String url = "jdbc:mysql://127.0.0.1/users";
    public static final String name = "com.mysql.jdbc.Driver";
    public static final String user = "root";
    public static final String password = "123456";
    public static final String sql = "select * from user";

    public static void main(String[] args) throws UnexpectedInputException, ParseException, Exception {
        Connection conn = null;
        PreparedStatement pst = null;
        ResultSet rs = null;
        try {
            // (1)注册驱动
            Class.forName(name);

            //(2) 获取链接
            conn = DriverManager.getConnection(url, user, password);

            // (3)准备语句
            pst = conn.prepareStatement(sql);

            // (4)执行查询
            rs = pst.executeQuery();

            // (5)迭代结果
            while (rs.next()) {
                System.out.println(rs.getString("username"));
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {

                if (rs != null) {
                    rs.close();
                }

                if (null == pst) {
                    pst.close();

                }

                if (null != conn) {
                    conn.close();

                }

            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

}

使用JDBC我们都知道按部就班的写(1)(2)的代码获取数据库链接,但是很少去研究这两个语句是干啥用的,特别是第一句,直接使用类加载器加载了驱动类到内存,这是何意?

三、原理

3.1 注册驱动

其实Class.forName(name);的作用是注册mysql驱动到驱动管理器,只有注册后,在(2)获取链接时候才能获取到mysql的数据库链接。Class.forName作用是加载类的字节码到内存生成Class对象,那么这里就是把类com.mysql.jdbc.Driver字节码加载到内存生成com.mysql.jdbc.Driver的Class对象,有了Calss对象就可以使用new了,或者Class.newInstance()了创建对象实例了。下面看看com.mysql.jdbc.Driver代码:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // 注册mysql驱动本身到驱动管理器
    //
    static {
        try {
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

在创建Driver的Class对象是会调用static块,注册当前驱动到驱动管理器。下面看看驱动管理器registerDriver方法:

public static synchronized void registerDriver(java.sql.Driver driver)
        throws SQLException {

        registerDriver(driver, null);
}

 public static synchronized void registerDriver(java.sql.Driver driver,
            DriverAction da)
        throws SQLException {

        //注册驱动到并发安全list
        if(driver != null) {
            registeredDrivers.addIfAbsent(new DriverInfo(driver, da));
        } else {
            // This is for compatibility with the original DriverManager
            throw new NullPointerException();
        }

        println("registerDriver: " + driver);

  }

其中registeredDrivers是个CopyOnWriteArrayList,是线程安全的list.

    private final static CopyOnWriteArrayList<DriverInfo> registeredDrivers = new CopyOnWriteArrayList<>();

一个问题:这里我们的main函数是使用AppClassLoader加载的,而DriverManager类则属于rt.jar(使用bootStarp加载),而调用类与被调用类使用的应该是同一个类加载器,那这里main函数为啥能调用DriverManager那,其实是因为Java的类加载委托机制。

3.2 获取连接

DriverManager类是驱动管理器类,里面也有个static块:

public class DriverManager {
    ...

    /**
     * Load the initial JDBC drivers by checking the System property
     * jdbc.properties and then use the {@code ServiceLoader} mechanism
     */
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
}
private static void loadInitialDrivers() {

        //获取jdbc.drivers属性里面的驱动名称
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        ...
        //使用SPI技术加载所有实现了java.sql.Driver的驱动类
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {

                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                ...
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        if (drivers == null || drivers.equals("")) {
            return;
        }

        //加载所有jdbc.drivers属性里面的驱动名称
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

关于SPI 参考:http://www.jianshu.com/p/a4dc755652ff 第三节
其实由于static块已经加载了所有驱动,(1)看似是可有可无的

(2)中从驱动管理器里面获取数据库连接,下面看下里面做了啥:



private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
   
    //(1)选择类加载器
    ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
    synchronized(DriverManager.class) {
        //如果类加载器为null,则使用线程上下文加载器
        if (callerCL == null) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
    }

    if(url == null) {
        throw new SQLException("The url cannot be null", "08001");
    }
    ...
    SQLException reason = null;
    
    //(2)遍历所有注册的驱动,找打一个可用的发起连接并返回
    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());
        }

    }

    // (3)到这里如果还没有获取连接,则返回失败原因
    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");
}


  • (1)设置类加载器,这里因为caller为TestJdbc.class,所以callerCL为AppClassLoader,
    这里如果callerCL为null,则使用线程上下文加载器,关于线程上下文加载器参考 http://www.jianshu.com/p/a4dc755652ff 的第三节。这给应用使用rt.jar包外的路径加载JDBC驱动提供了途径。

  • (2)遍历注册的所有驱动,找到一个使用callerCL加载器能加载成功的驱动,获取连接。

其中鉴权代码为:

private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    boolean result = false;
    if(driver != null) {
        Class<?> aClass = null;
        try {
            aClass =  Class.forName(driver.getClass().getName(), true, classLoader);
        } catch (Exception ex) {
            result = false;
        }

         result = ( aClass == driver.getClass() ) ? true : false;
    }

    return result;
}

使用callerCL类加载器去加载driver,然后判断当前加载的Class和driver的Class是否是同一个对象,是则成功,否者失败。这里注册Driver时候类加载器为BootStrap,而callerCL是AppClassloader,由于委托机制,所以这里返回true.也就是注册驱动时候类加载器和使用时候必须是同一个或者具有委托关系时候才会鉴权成功,也就是说才有权限去调用驱动的connect方法。

另外比如我们调用:
Class.forName("oracle.jdbc.driver.OracleDriver");
Class.forName("com.mysql.jdbc.Driver");
注册了两个驱动到驱动管理器,那么当调用DriverManager.getConnection(url, user, password);传递mysql数据库的url,user,password时候,假如第一次返回的是oracle的驱动,则调用connect时候会返回SQLException异常,然后循环获取注册的下一个驱动,返回mysql驱动后则connect 成功返回数据库链接。

  • (3)如果没有可用的驱动则返回错误信息。

想获取更多技术干货,请关注微信公众号:‘技术原始积累’

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,185评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,652评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,524评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,339评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,387评论 6 391
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,287评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,130评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,985评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,420评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,617评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,779评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,477评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,088评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,716评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,857评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,876评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,700评论 2 354

推荐阅读更多精彩内容