你真的的懂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)如果没有可用的驱动则返回错误信息。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏黑泽君的专栏

day06_JDBC学习笔记

  JDBC:Java DataBase Connectivity,是SUN公司提供的一套操作数据库的标准规范(技术)。

9420
来自专栏Java面试笔试题

JDBC能否处理Blob和Clob?

Blob是指二进制大对象(Binary Large Object),而Clob是指大字符对象(Character Large Objec),因此其中Blob是为...

30850
来自专栏史上最简单的Spring Cloud教程

如何使用MongoDB+Springboot实现分布式ID?

一、背景 如何实现分布式id,搜索相关的资料,一般会给出这几种方案: 使用数据库自增Id 使用reids的incr命令 使用UUID Twitter的snowf...

28550
来自专栏软件开发

JavaSE学习总结(九)—— Java访问数据库(JDBC)

一、JDBC简介 JDBC(Java Data Base Connectivity,java数据库连接)是一种用于执行SQL语句的Java API,可以为多种关...

22550
来自专栏九彩拼盘的叨叨叨

async/await 写法示例

async/await 让写异步代码感觉像写同步代码。async/await 并不是 ES6 的一部分,但可以通过使用 Babel 来使用它。

9320
来自专栏java学习

Java每日一练(2017/6/8)

Java基础 | 数据库 | Android | 学习视频 | 学习资料下载 课前导读 ●回复"每日一练"获取以前的题目! ●答案公布时间:为每期发布题目的第二...

28070
来自专栏TechBox

一份走心的iOS开发规范前言约定(一)命名规范(二)编码规范2.14 内存管理规范本文参考文章其他有价值的文章

67890
来自专栏weixuqin 的专栏

JDBC技术

任何一种数据库驱动程序都提供一个 java.sql.Driver 接口的驱动类,在加载某个数据库驱动程序的驱动类时,都创建自己的实例对象并向 java.sql....

11220
来自专栏用户2442861的专栏

Java对MySQL数据库进行连接、查询和修改

http://www.cnblogs.com/aniuer/archive/2012/09/10/2679241.html

9720
来自专栏知识分享

JDBC基本知识

JDBC的作用 JDBC为java访问数据库提供通用的API,可以为多种关系数据库提供统一访问。因为SQL是关系式数据库管理系统的标准语言,只要我们遵循SQL规...

371110

扫码关注云+社区

领取腾讯云代金券