datasource获取url_自定义DataSource

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

主要内容:

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

数据源的作用

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

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

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

而数据源的做法是:

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

datasource获取url_自定义DataSource_第1张图片

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

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


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

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

datasource获取url_自定义DataSource_第2张图片
假设用户通过数据源的close关闭Connection,那么这个方法可以内部调用连接池归还,而不是实际关闭

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

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

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


自定义数据源:动态代理

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

  • 数据源>=连接池

datasource获取url_自定义DataSource_第3张图片
  • 数据源返回的Connection是代理对象

datasource获取url_自定义DataSource_第4张图片

调用代理对象的每一个方法,最终都会去调用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;
	}

}

画了几幅示意图:

datasource获取url_自定义DataSource_第5张图片

datasource获取url_自定义DataSource_第6张图片
之所以连接池剩3个Connection时会创建新的连接存入,是因为程序中设定空闲连接小于minIdleCount(3)时要创建

datasource获取url_自定义DataSource_第7张图片

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


DBCP连接池部分源码解析

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

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

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

close方法

datasource获取url_自定义DataSource_第8张图片

passivate方法(钝化连接)

datasource获取url_自定义DataSource_第9张图片
最终一定会把_close设为true,代表连接已关闭

preparedStatement

datasource获取url_自定义DataSource_第10张图片
使用conn获取preparedStatement之前会调用checkOpen方法

datasource获取url_自定义DataSource_第11张图片

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

datasource获取url_自定义DataSource_第12张图片

25e48ef14a0e4b4a60e8832c0f44cbe3.png

2019-5-7 15:40:17

你可能感兴趣的:(datasource获取url)