• 在JDBC(下)我会引入DBCP或者C3P0数据源来完善JdbcUtils。所以这里插播一下。

主要内容:

  • 数据源的作用
  • 为什么用代理模式/装饰者模式
  • 自定义数据源:动态代理
  • DBCP连接池部分源码解析

------

## 数据源的作用

之前提过,JDBC操作数据库,底层走的还是TCP协议。虽然我没专门学过计算机网络,但是也知道频繁开闭网络连接的时间开销是很大的,比如“三次握手”啥的。而数据源就是为了解决频繁创建销毁Connection所产生的时间开销问题。

在我看来,数据源最大的作用就是“复用Connection,减少时间开销”。

什么意思呢?每当我们调用DriverManager.getConnection(),底层会去调用driver.connect(),而connect()方法再往下就是很细节的网络连接。也就是说,DriverManager.getConnection()每次获取Connection,都会经历TCP的“三次握手”,以及数据库的各种校验,效率相当低。

而数据源的做法是:

  • 项目启动时初始化固定数量的Connection,把创建连接的时间开销提前
  • 用完Connection不是直接关闭,而是归还到连接池
  • 当连接池现有的Connection不够时,才会去进行耗时的Connection创建

img

也就是说,连接池把创建10个Connection的时间开销提前到项目启动时,而不是等用户访问时才创建。如此一来,用户访问数据库的时间开销从原先的1s(创建)降低到了0.1s(从池中拿)。

而且程序用完Connection后调用connection.close()并不是销毁它,而是归还给连接池,达到了复用。

------

## 为什么用代理模式/装饰者模式

如果我们是直接调用数据源的close()方法,那完全可以把Connection归回给自身维护的连接池,确实用不到动态代理或者装饰者模式。

img假设用户通过数据源的close关闭Connection,那么这个方法可以内部调用连接池归还,而不是实际关闭

但是,难就难在我们返回给用户的往往就是一个Connection对象,即

Conncetion conn = dataSource.getConnection();
一顿骚操作之后...
conn.close();

而Connection本身close()的做法是:销毁连接。所以,我们必须“偷天换日”,悄悄地把connection.close()方法变成“将Connection归还连接池”,而不是实际关闭。

------

## 自定义数据源:动态代理

首先,我们来明确两个要点:

  • 数据源>=连接池

img

  • 数据源返回的Connection是代理对象

img

调用代理对象的每一个方法,最终都会去调用invocationHandler的invoke()方法。我们只需在invoke()中判断当前调用是否为close方法(Method.getName())。如果是,则拦截它并把close改为“将Connection归还连接池”。

对动态代理比较陌生的朋友,可以看看这个回答:Java 动态代理作用是什么?

总共就写了两个类,一个是自定义DataSource,一个是测试类。

可能对于新手来说难度较大,最好自己复制到本地IDEA,一边调试一边看。

DataSourceTest

public class DataSourceTest {
    public static void main(String[] args) throws SQLException {
                // 创建连接池对象
        MyDatasource datasource = new MyDatasource();

                // 用来存储待关闭的连接
        List<Connection> connectionsToBeClosed =  new ArrayList<Connection>();

                // 循环多次从连接池取出连接
        for (int i = 0; i < 11; i++) {
            System.out.println();
            System.out.println("------第"+ (i+1) +"次-------");

            Connection conn = datasource.getConnection();
            System.out.println("使用Connection:" + conn);
            // 1.创建sql模板
            String sql = "select * from t_user where age = ?";

            PreparedStatement preparedStatement = conn.prepareStatement(sql);

            // 2.设置模板参数
            preparedStatement.setInt(1, 18);

            // 3.执行语句
            ResultSet rs = preparedStatement.executeQuery();

            // 4.处理结果
            while (rs.next()) {
                System.out.println(rs.getObject(1) + "\t" + rs.getObject(2) + "\t"
                        + rs.getObject(3) + "\t" + rs.getObject(4));
            }

            // 5.收集连接
            connectionsToBeClosed.add(conn);
        }

        /*
        *  集中释放连接,会产生一个现象:
        *  程序执行到这里,连接池中已有若干个连接
                *  但还没到maxIdleCount,所以此时conn.close是归还池中
        *  
                *  从第7次起,conn.close就会真的关闭Connection,因为连接池满了
        * */
        for (int i = 0; i < 11; i++) {
            connectionsToBeClosed.get(i).close();
        }


    }
}

MyDataSource

/**
 * 数据源,包含一个连接池
 * 连接池里的存放的Connection以及用户从数据源拿走的其实都是【代理连接】,它的close方法其实是“归还池中”
 */
public class MyDatasource {

    // 数据库信息,用于连接数据库
    private static String url = "jdbc:mysql://192.168.136.128:3306/test?useSSL=false";
    private static String user = "root";
    private static String password = "root";

    /*
     * 【特别注意】
     *
     * 数据源(DataSource):负责生产连接,它内部有一个连接池
     * 连接池(ConnectionsPool):它只是个容器,用于存放数据源生成的连接,本身不能创建连接
     *
     * 空闲连接(Idle Connection):在连接池中的连接。用户从池中拿走,正在使用的是“忙碌”的连接
     *
     * initCount:new MyDataSource()时,往池中预先存入initCount个连接(也算空闲连接)
     *
     * minIdleCount:连接池最小空闲连接数,少于这个值就要创建Connection存入池中
     *
     * maxIdleCount:连接池最大空闲连接数。和数据源能产生多少个连接无关。
     *               连接池最多能存10个,但是数据源可以生产第11个。第11个无法归还池中?无所谓,直接销毁
     *
     * currentIdleCount:当前存活的连接数(池中空闲+用户拿去的)
     *
     * */

    // 池中初始连接数(创建DataSource时池中就有5个连接)
    private static int initCount = 5;
    // 池中最小空闲连接数,小于这个数量就要创建连接并加入池中
    private static int minIdleCount = 3;
    // 池中最大允许存放的连接数
    private static int maxIdleCount = 10;
    // 当前池中连接数
    private static int currentIdleCount = 0;
    // 数据源创建连接的次数
    private static int createCount = 0;

    // LinkedList充当连接池,removeFirst取出连接,addLast归还连接
    private final static LinkedList<Connection> connectionsPool = new LinkedList<Connection>();

    /**
     * 空参构造,按照initCount预先创建一定数量的连接存入池中
     */
    public MyDatasource() {
        try {
            for (int i = 0; i < initCount; i++) {
                // 创建RealConnection
                Connection realConnection = DriverManager.getConnection(url, user, password);
                // 将RealConnection传入createProxyConnection(),得到代理连接并加入池中,currentIdleCount++
                this.connectionsPool.addLast(this.createProxyConnection(realConnection));
                currentIdleCount++;
            }
            System.out.println("-------连接池初始化结束,共初始化" + this.currentIdleCount + "个Connection-------");
        } catch (SQLException e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    /**
     * 公共方法,外界通过MyDataSource调用此方法得到代理连接
     *
     * @return
     * @throws SQLException
     */
    public Connection getConnection() throws SQLException {
        //同步代码
        synchronized (connectionsPool) {

            // 连接池中还有空闲连接,从池中取出,currentIdleCount--
            if (currentIdleCount > 0) {
                currentIdleCount--;
                if (currentIdleCount < minIdleCount) {
                    // 创建RealConnection
                    Connection realConnection = DriverManager.getConnection(url, user, password);
                    // 将RealConnection传入createProxyConnection(),得到代理连接并加入池中,currentIdleCount++
                    this.connectionsPool.addLast(this.createProxyConnection(realConnection));
                    currentIdleCount++;
                }
                return this.connectionsPool.removeFirst();
            }

            /*
             *  如果连接池没有空闲连接(都被用户拿走了),那么就再生成连接。比如第11个。
             *  不用考虑maxIdleCount,它指的是连接池最多存放多少个空闲连接,而不是数据源能生成多少个。
             *  如果这第11个连接后期调用close,程序会判断当前连接池中的连接数是否大于maxIdleCount,
                         *  如果已经存满了就直接销毁第11个连接,不会放入池中
             * */
            Connection realConnection = DriverManager.getConnection(url, user, password);
            // 数据源创建连接后直接返回,没有加入池,也没有从池中取出,currentIdleCount不变
            return this.createProxyConnection(realConnection);
        }
    }

    /**
     * 私有方法,用于生成代理连接
     * 调用时机:数据源初始化,以及用户调用dataSource.getConnection时
     *
     * @param realConn
     * @return
     * @throws SQLException
     */
    private Connection createProxyConnection(Connection realConn) throws SQLException {
        // 这句代码仅仅是为了把realConn转为final,这样才能在匿名对象invocationHandler中使用
        final Connection realConnection = realConn;

        // 动态代理:返回Connection代理对象
        Connection proxyConnection = (Connection) Proxy.newProxyInstance(
                this.getClass().getClassLoader(),
                realConnection.getClass().getInterfaces(),
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        // 对close()方法进行拦截
                        if ("close".equals(method.getName())) {
                            // 连接池空闲连接数小于最大空闲数,说明还能存得下,于是连接被归还到池中
                            if (MyDatasource.currentIdleCount < MyDatasource.maxIdleCount) {
                                MyDatasource.connectionsPool.addLast((Connection) proxy);
                                MyDatasource.currentIdleCount++;
                                // 返回1表示成功
                                return 1;
                            } else {
                                // 当前连接池满了,这个连接已经存不下,所以只能销毁(调用目标对象的close)
                                realConnection.close();
                                // 返回1表示成功
                                return 1;
                            }
                        }
                        return method.invoke(realConnection, args);
                    }
                });

        System.out.println("新建Connection(" + (++MyDatasource.createCount) + "):" + proxyConnection);
        return proxyConnection;
    }

}

画了几幅示意图:

img

img之所以连接池剩3个Connection时会创建新的连接存入,是因为程序中设定空闲连接小于minIdleCount(3)时要创建

img

当然了,也可以使用装饰者模式包装realConnection,而且装饰者模式要好理解很多。就是代码要多写一点。

------

## DBCP连接池部分源码解析

我自己定义的连接池,虽然利用动态代理偷偷替换了conn.close(),即使调用也不会直接关闭,而是归还连接池。但归还后,其实还可以继续使用。毕竟我还是持有conn的引用,不管它是不是在池中。

Connection conn = datasource.getConnection();
// 一顿骚操作
conn.close();
// 继续拿conn得到preparedStatement一顿骚操作

而DBCP等连接池,则要完善地多,在调用conn.close后确实也将连接归还到连接池了,但再次使用会抛异常。具体原因在于内部的状态量检查:

close方法

img

passivate方法(钝化连接)

img最终一定会把_close设为true,代表连接已关闭

preparedStatement

img使用conn获取preparedStatement之前会调用checkOpen方法

img

测试(DBCP使用已关闭的conn):

img

img