我记得18年6月开始JavaWeb系列的写作,截止目前,还剩下JDBC和Filter/Listener。速度有点超乎预期地慢...不过随着自己开发经验的增长以及对知识点的理解,相信后面出活会越来越快。

JDBC本身是非常简单的,就是一组API接口,只要导入对应的驱动包(JDBC实现类)就可以对数据库进行增删改查。但是其中涉及到很多问题,比如:

  • Connection的管理(连接池)
  • 冗余代码的抽取(获取、释放连接)
  • 结果集的封装(映射Pojo字段)

等等。不论培训班视频还是书籍,讲到JDBC时都会把上面的内容串联起来,这使得它的难度瞬时翻了几番。

主要内容:

  • JDBC简介
  • 基本环境搭建
  • Driver
  • DriverManager
  • 代码优化

------

## JDBC简介

在说JDBC之前,必须先聊聊数据持久化。

持久化

把数据保存到可掉电式存储设备中以供之后使用。

大多数情况下,数据持久化意味着将内存中的数据保存到磁盘中加以“固化”。而持久化的实现过程大多通过各种关系数据库完成。当然,也可以存入磁盘文件或者XML数据文件(崔老师JavaWeb day14练习中,使用了XML充当“数据库”)。

img图片来自尚硅谷JDBC

JDBC

数据库是实现持久化的一种途径,而JDBC则是通向数据库的桥梁。

通俗地讲,JDBC就是一组API(包括少量类),为访问不同数据库提供了统一的途径,为开发者屏蔽了一些细节问题。比如,我们都知道浏览器发送HTTP请求访问服务器,但其实请求底层仍是TCP协议。同样的,访问数据库底层也通过TCP协议。你知道怎么与数据库建立TCP连接吗?一部分科班读者可能对计算机网络非常熟悉,但是大部分像我这样的野生程序员可能压根没想过这个问题。所幸,这些具体的实现,各大数据库产商已经替我们做了。

驱动

JDBC是Java制定的接口,数据库产商依照该接口编写与自家数据库配套的实现类。比如MySQL、Oracle、SqlServer等都有自己的不同实现,这些实现类的集合既是我们笼统意义上的“驱动”。

img

面向接口编程

在代码中直接new具体的驱动类,会使程序高度耦合。比如,后期如果要切换数据库(虽然很少),就要临时调换驱动类,需要修改源码,不符合开闭原则。而面向接口编程,实际上就是一种“多态”。屏蔽具体的实现,只需调用接口方法,传入规定的参数即可得到预期的返回值。切换数据库驱动并不影响程序运行结果。

------

## 基本环境搭建

上面说过,JDBC只是一组接口,具体实现交给驱动。所以,要使用JDBC完成CRUD,必须先导入具体的数据库驱动。本次我们以MySQL为例,所以导入MySQL的数据库驱动。

pom.xml

    <dependencies>
        <!--MySQL数据库驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.46</version>
        </dependency>
        <!--Junit-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

代码结构

img

DriverDemo

public class DriverDemo {
    @Test
    public void testDriver() throws SQLException {
        //1. 创建一个 Driver 实现类的对象
        Driver driver = new com.mysql.jdbc.Driver();

        //2. 准备连接数据库的基本信息: url, user, password
        String url = "jdbc:mysql://192.168.136.128:3306/test";
        Properties info = new Properties();
        info.put("user", "root");
        info.put("password", "root");

        //3. 调用 Driver 接口的 connect(url, info) 获取数据库连接
        Connection connection = driver.connect(url, info);
        System.out.println(connection);
    }
}

运行结果

img

使用JDBC有三个大步骤:

  • 连接数据库
  • 执行SQL语句
  • 获得结果

我们上面的代码,仅完成了第一步。

------

## Driver

上面testDriver()方法中的第一句:

//1. 创建一个 Driver 实现类的对象
Driver driver = new com.mysql.jdbc.Driver();

这是一个典型的面向接口编程。

我们先看左边的Driver接口:

img接口的方法并不多,我们在上面用到了其中的connect()方法

接着我们再来看看右边MySQL的Driver实现类:

img

怎么回事?!

只有一个静态代码块和无参构造。不对啊,我们明明在程序中调用了driver.connect(),怎么连connect方法都没了?!

仔细一看,原来MySQL的Driver类还继承了NonRegisteringDriver,它实现了Driver接口的全部方法:

img

connect()的核心代码就一句:

Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url);

总之,就是根据给定的url和用户名密码,返回一个与数据库关联的Connection。JDBC相当于是程序与数据库之间的桥梁。得到Connect代表桥已经建好,此刻已经可以通车了。

img

由于com.mysql.jdbc.Driver继承了NonRegisteringDriver,所以它也可以调用connect方法(继承父类方法),上面我们已经用过,确实可行。

现在我们把注意力集中到com.mysql.jdbc.Driver中独有的代码:静态代码块+无参构造。

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!");
        }
    }

    /**
     * Construct a new driver and register it with DriverManager
     * 
     * @throws SQLException
     *             if a database error occurs.
     */
    public Driver() throws SQLException {
        // Required for Class.forName().newInstance()
    }
}

空参构造的作用,仅仅是为了能够new com.mysql.jdbc.Driver()。但重点不在于new本身,而是new这个Driver的时候,JVM会去加载这个Driver实现类。此时会自动执行静态代码块中的代码,也就是:

java.sql.DriverManager.registerDriver(new Driver());

注释已经说得很明白,就是把这个Driver注册到DriverManager。

所以,我们使用Driver获取连接的整体逻辑是:

  • new com.mysql.jdbc.Driver()
  • 类被加载进内存
  • 静态代码块执行:java.sql.DriverManager.registerDriver(newDriver());
  • com.mysql.jdbc.driver实例被注册进DriverManager
  • 调用com.mysql.jdbc.driver实例的connect()方法获得Connection

new com.mysql.jdbc.driver();最终目的是为了driver.connect()获得Connection,这一点很合理。但是为啥还要在类中搞一个静态代码块去注册驱动?没有这一步,我不照样获得Connection吗?

------

DriverManager

其实,之所以com.mysql.jdbc.Driver类中的静态代码块要去注册驱动,是因为获取Connection通常有两种办法:

  • 根据具体的Driver实现类直接获取
  • 通过驱动管理器获取

通常后者更常用。而所谓的驱动管理器,就是DriverManager。通过DriverManager.getConnection()即可获取。

此刻,我们发现脑中瞬间多了两个疑问:

  1. DriverManager是个啥呀?咋还能getConnection()了呢?
  2. 还有,将Driver注册进DriverManager又能咋地?driver.connect()不也好使吗,照样获得了Connection,为啥要整出一个DriverManager?

不急,先上代码,看看DriverManager如何获得Connection。

    @Test
    public void testDriverManager() throws Exception{
        //1. 驱动的全类名
        String driverClass = "com.mysql.jdbc.Driver";
        //2. 准备连接数据库的基本信息: url, user, password
        String url = "jdbc:mysql://192.168.136.128:3306/test";
        String user = "root";
        String password = "root";

        //2. 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
        Class.forName(driverClass);

        //3. 通过 DriverManager 的 getConnection() 方法获取数据库连接
        Connection connection =
                DriverManager.getConnection(url, user, password);
        System.out.println(connection);

    }

上面示例中,最关键的一句代码是:

//2. 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
Class.forName(driverClass);

JVM何时会加载一个类呢?

大体可以归为两种情况:

被动加载(Driver方式)

//JVM发现要new一个com.mysql.jdbc.Driver实例,而此时内存中没有com.mysql.jdbc.Driver字节码对象,就会去加载该类
Driver driver  =  new com.mysql.jdbc.Driver();

主动加载(DriverManager方式)

//通过Class对象主动触发JVM加载com.mysql.jdbc.Driver类
Class.forName("com.mysql.jdbc.Driver");

我们已经分析过,不论是主动还是被动,只要类被加载,静态代码块都会执行执行。也就是说,com.mysql.jdbc.Driver都会被注册进DriverManager。

Driver方式获取Connection确实和“注册”操作没有直接关系,但是DriverManager方式与“注册”关系密切。我们来分析一下源码。

我们之前已经查看过com.mysql.jdbc.Driver的源码,知道了connect()方法的具体实现(继承自NonRegisteringDriver):

img这一句代码,底层屏蔽了好多细节,直接连上数据库返回了Connection

也知道了com.mysql.jdbc.Driver会通过静态代码块,将自己注册到了DriverManager。

那么,问题就来了:driver.connect()和DriverManager.getConnection()都可以获得连接,它们之间有什么联系吗?

img1.DriverManager内部维护一个容器,该容器存储所有已注册的Driver

img2.通过DriverManager也可以得到连接

来看一下这个同名的getConnection()方法具体做了啥:

    //私有方法,只能内部调用:获取数据库连接
    private static Connection getConnection(
        String url, java.util.Properties info, Class<?> caller) throws SQLException {
        //callerCl是类加载器
        ClassLoader callerCL = caller != null ? caller.getClassLoader() : null;
        //同步代码块
        synchronized(DriverManager.class) {
            if (callerCL == null) {
                //如果没有传入类加载器,使用当前线程的类加载器
                callerCL = Thread.currentThread().getContextClassLoader();
            }
        }

        if(url == null) {
            throw new SQLException("The url cannot be null", "08001");
        }

        println("DriverManager.getConnection(\"" + url + "\")");

        SQLException reason = null;

        //循环遍历容器中所有已注册的Driver
        for(DriverInfo aDriver : registeredDrivers) {
            //isDriverAllowed()方法会使用callerCl尝试加载每一个Driver,加载成功返回true
            if(isDriverAllowed(aDriver.driver, callerCL)) {
                try {
                    println("    trying " + aDriver.driver.getClass().getName());
                    //底层还是使用了Driver本身的connect方法,获取Connection
                    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");
    }


    //加载驱动类,加载失败返回false
    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;
    }

小结

DriverManager是个啥呀?咋还能getConnection()了呢?

DriverManager是java.sql包下的一个类,内部维护着一个CopyOnWriteArrayList用于存放已经注册的驱动实例。之所以能getConnection(),是因为底层会循环遍历所有驱动,找到当前注册的驱动后调用driver.connect()获得Connection。

还有,将Driver注册进DriverManager又能咋地?driver.connect()不也好使吗,照样获得了Connection,为啥要整出一个DriverManager?

其实也没为啥,人家设计的初衷就是为了支持注册多个(多种)驱动,通过DriverManager可以管理多个驱动程序。所以它叫“驱动管理器”(DriverManager)。

静态代码块注册驱动到DriverManager对于Driver方式获取Connection是多余的,但是对于DriverManager获取Connection是必须的。

img

------

## 代码优化

把url、user、password、driver移到jdbc.properties文件中,做成可配置。

    @Test
    public void getConnection() throws Exception{
        //1. 准备连接数据库的 4 个字符串.
        //1). 创建 Properties 对象
        Properties properties = new Properties();

        //2). 获取 jdbc.properties 对应的输入流
        InputStream in =
                this.getClass().getClassLoader().getResourceAsStream("jdbc.properties");

        //3). 加载 2) 对应的输入流
        properties.load(in);

        //4). 具体决定 user, password 等4 个字符串.
        String user = properties.getProperty("user");
        String password = properties.getProperty("password");
        String jdbcUrl = properties.getProperty("jdbcUrl");
        String driver = properties.getProperty("driver");

        //2. 加载数据库驱动程序(对应的 Driver 实现类中有注册驱动的静态代码块)
        Class.forName(driver);

        //3. 通过 DriverManager 的 getConnection() 方法获取数据库连接
        Connection connection = DriverManager.getConnection(jdbcUrl, user, password);

        System.out.println(connection);
    }

img

JDBC的三大步骤:

  • 连接数据库
  • 执行SQL语句
  • 获得结果

这一篇只涉及如何获取Connection,下篇才真正开始CRUD以及代码抽取。

最后,我自己看源码时,发现一个问题:Driver接口有个getParentLogger(),但是NonRegisteringDriver并没有实现这个方法(其他方法都实现了)。

这是为啥呢?

img打开源码,IDEA提示错误...