在JDBC(下)我会引入DBCP或者C3P0数据源来完善JdbcUtils。所以这里插播一下。
主要内容:
之前提过,JDBC操作数据库,底层走的还是TCP协议。虽然我没专门学过计算机网络,但是也知道频繁开闭网络连接的时间开销是很大的,比如“三次握手”啥的。而数据源就是为了解决频繁创建销毁Connection所产生的时间开销问题。
在我看来,数据源最大的作用就是“复用Connection,减少时间开销”。
什么意思呢?每当我们调用DriverManager.getConnection(),底层会去调用driver.connect(),而connect()方法再往下就是很细节的网络连接。也就是说,DriverManager.getConnection()每次获取Connection,都会经历TCP的“三次握手”,以及数据库的各种校验,效率相当低。
而数据源的做法是:
也就是说,连接池把创建10个Connection的时间开销提前到项目启动时,而不是等用户访问时才创建。如此一来,用户访问数据库的时间开销从原先的1s(创建)降低到了0.1s(从池中拿)。
而且程序用完Connection后调用connection.close()并不是销毁它,而是归还给连接池,达到了复用。
如果我们是直接调用数据源的close()方法,那完全可以把Connection归回给自身维护的连接池,确实用不到动态代理或者装饰者模式。
假设用户通过数据源的close关闭Connection,那么这个方法可以内部调用连接池归还,而不是实际关闭但是,难就难在我们返回给用户的往往就是一个Connection对象,即
Conncetion conn = dataSource.getConnection();
一顿骚操作之后...
conn.close();
而Connection本身close()的做法是:销毁连接。所以,我们必须“偷天换日”,悄悄地把connection.close()方法变成“将Connection归还连接池”,而不是实际关闭。
首先,我们来明确两个要点:
调用代理对象的每一个方法,最终都会去调用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 connectionsToBeClosed = new ArrayList();
// 循环多次从连接池取出连接
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 connectionsPool = new LinkedList();
/**
* 空参构造,按照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;
}
}
画了几幅示意图:
之所以连接池剩3个Connection时会创建新的连接存入,是因为程序中设定空闲连接小于minIdleCount(3)时要创建当然了,也可以使用装饰者模式包装realConnection,而且装饰者模式要好理解很多。就是代码要多写一点。
我自己定义的连接池,虽然利用动态代理偷偷替换了conn.close(),即使调用也不会直接关闭,而是归还连接池。但归还后,其实还可以继续使用。毕竟我还是持有conn的引用,不管它是不是在池中。
Connection conn = datasource.getConnection();
// 一顿骚操作
conn.close();
// 继续拿conn得到preparedStatement一顿骚操作
而DBCP等连接池,则要完善地多,在调用conn.close后确实也将连接归还到连接池了,但再次使用会抛异常。具体原因在于内部的状态量检查:
close方法
passivate方法(钝化连接)
最终一定会把_close设为true,代表连接已关闭preparedStatement
使用conn获取preparedStatement之前会调用checkOpen方法测试(DBCP使用已关闭的conn):
2019-5-7 15:40:17