1. 自定义连接池
连接池概念
1. 为什么要使用连接池
Connection对象在JDBC使用的时候就会去创建一个对象,使用结束以后就会将这个对象给销毁了(close).每次创建和销毁对象都是耗时操作.
这个时候,我们就需要使用连接池对其进行优化.
程序初始化的时候,初始化多个连接,将多个连接放入到池(集合)中.每次获取的时候,都可以直接从连接池中进行获取.使用结束以后,将连接归还到池中.
2. 生活里面的连接池例子
- 老方式: 下了地铁需要骑车, 跑去生产一个, 然后骑完之后,直接把车销毁了.
- 连接池方式 摩拜单车: 骑之前, 有一个公司生产了很多的自行车, 下了地铁需要骑车, 直接扫码使用就好了, 然后骑完之后, 还回去。这样就可以避免每个人都去弄个自行车,减少了消耗。
3. 连接池原理
- 程序一开始就创建一定数量的连接,放在一个容器(集合)中,这个容器称为连接池。
- 使用的时候直接从连接池中取一个已经创建好的连接对象, 使用完成之后 归还到池子
- 如果池子里面的连接使用完了, 还有程序需要使用连接, 先等待一段时间(eg: 3s), 如果在这段时间之内有连接归还, 就拿去使用; 如果还没有连接归还, 新创建一个, 但是新创建的这一个不会归还了
- 集合选择LinkedList
- 增删比较快
- LinkedList里面的removeFirst()和addLast()方法和连接池的原理吻合
下面我们来写几个示例,演示一下连接池的基础用法。
自定义连接池-初级版本
1.目标
根据连接池的原理, 使用 LinkedList 自定义连接池
2.分析
- 创建一个类 MyDataSource, 定义一个集合 LinkedList
- 程序 初始化 的时候, 创建 5个连接 存到 LinkedList
- 定义 getConnection() 从 LinkedList 取出 Connection 返回
- 定义 addBack() 方法归还 Connection 到 LinkedList
3.实现
3.1 创建一个类 MyDataSource, 定义一个集合 LinkedList
public class MyDataSource {
// 定义一个集合 LinkedList 用来存储连接
LinkedList<Connection> connectPool = new LinkedList<>();
}
3.2 程序 初始化 的时候, 创建 5个连接 存到 LinkedList
public class MyDataSource {
// 定义一个集合 LinkedList 用来存储连接
LinkedList<Connection> connectPool = new LinkedList<>();
// 程序 初始化 的时候, 创建 5个连接 存到 LinkedList
public MyDataSource() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Li*******密码*******0";
Connection conn = DriverManager.getConnection(url, user, password);
//将连接添加到connectionPool中
connectPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
3.3 定义 getConnection() 从 LinkedList 取出 Connection 返回
// 定义 getConnection() 从 LinkedList 取出 Connection 返回
public Connection getConnection(){
//从容器中获取连接
Connection conn = connectPool.removeFirst();
return conn;
}
3.4 定义 addBack() 方法归还 Connection 到 LinkedList
// 定义 addBack() 方法归还 Connection 到 LinkedList
public void addBack(Connection connection){
connectPool.addLast(connection);
}
3.5 当前完成代码
public class MyDataSource {
// 定义一个集合 LinkedList 用来存储连接
LinkedList<Connection> connectPool = new LinkedList<>();
// 程序 初始化 的时候, 创建 5个连接 存到 LinkedList
public MyDataSource() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Li*************10";
Connection conn = DriverManager.getConnection(url, user, password);
//将连接添加到connectionPool中
connectPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 定义 getConnection() 从 LinkedList 取出 Connection 返回
public Connection getConnection(){
//从容器中获取连接
Connection conn = connectPool.removeFirst();
return conn;
}
// 定义 addBack() 方法归还 Connection 到 LinkedList
public void addBack(Connection connection){
connectPool.addLast(connection);
}
}
3.6 编写测试方法,创建多个连接测试使用如下:
可以看到能够正常获取到 5 个 数据库连接,那么如果再创建多一个连接呢?
3.7 尝试创建超过连接池的数量
可以看到,当我们执行到 第 6 个连接的时候,则会报错:提示没有该元素。
3.8 异常的原因:由于连接池的 LinkedList 只有 5 个元素,当获取到第 6 个的时候,由于 LinkedList 的元素都被清空了,再去获取的时候会执行 removeFirst() 方法,导致报错。
解决的办法:如果要避免异常,那么就要当获取的连接数 超过 连接池的数量的时候,则重新创建一个连接,避免异常。
修改如下:
// 定义 getConnection() 从 LinkedList 取出 Connection 返回
public Connection getConnection() {
//从容器中获取连接
Connection conn = null;
if (connectPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Li**************0";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}
3.9 再次尝试获取 6 个连接,查看能否正常获取连接
3.10 测试在使用完毕连接之后,将连接重新设置回连接池中
可以看到 addBack()
方法能够将连接放入 连接池,但是却把 额外新增的 第 6 个连接也放入了连接池。这就不太好了。
但是我们可以发现,addBack()
方法由于无法识别这个连接是原来就创建好,还是额外新增的,导致都会将其放入到连接池中。
另外,也存在一个问题,那就是当我们希望切换使用其他连接池的时候,由于我们的连接池是自定义的,耦合性太高,不方便项目切换使用其他连接池。
3.11 完整示例代码
/**
*
* 自定义连接池的第一个版本
* 1. 创建一个容器,存放连接
* 2. 默认往容器中存放5个连接
* 在构造函数中编写代码
* 3. 提供一个方法,让调用者获取连接
* 4. 提供一个方法,让调用者归还连接
*
* 当前第一个版本存在的问题:
* 1. 新创建的连接(原本没有在连接池中的连接)也会归还回连接池
* 2. 连接池使用的耦合性太高了,不便于以后项目切换连接池
*
* @author Aron.li
* @date 2021/1/25 0:39
*/
public class MyDataSource {
// 定义一个集合 LinkedList 用来存储连接
LinkedList<Connection> connectPool = new LinkedList<>();
// 程序 初始化 的时候, 创建 5个连接 存到 LinkedList
public MyDataSource() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lijw********610";
Connection conn = DriverManager.getConnection(url, user, password);
//将连接添加到connectionPool中
connectPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
// 定义 getConnection() 从 LinkedList 取出 Connection 返回
public Connection getConnection() {
//从容器中获取连接
Connection conn = null;
if (connectPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lijw**********!610";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}
// 定义 addBack() 方法归还 Connection 到 LinkedList
public void addBack(Connection connection) {
connectPool.addLast(connection);
}
// 编写测试方法,创建多个连接测试使用如下:
@Test
public void test01() {
// 测试创建5个连接
MyDataSource dataSource = new MyDataSource();
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
Connection connection3 = dataSource.getConnection();
Connection connection4 = dataSource.getConnection();
Connection connection5 = dataSource.getConnection();
// 超出连接池的数量
Connection connection6 = dataSource.getConnection();
// 测试在使用完毕连接之后,将连接重新设置回连接池中
dataSource.addBack(connection1);
dataSource.addBack(connection2);
dataSource.addBack(connection3);
dataSource.addBack(connection4);
dataSource.addBack(connection5);
dataSource.addBack(connection6);
}
}
自定义连接池-进阶版本
1.目标
实现datasource完成自定义连接池
2.分析
在初级版本版本中, 我们定义的方法是getConnection(). 因为是自定义的.如果改用李四的自定义的连接池,李四定义的方法是getAbc(), 那么我们的源码就需要修改, 这样不方便维护. 所以sun公司定义了一个接口datasource,让自定义连接池有了规范
3. datasource接口概述
Java为数据库连接池提供了公共的接口:javax.sql.DataSource,各个厂商(用户)需要让自己的连接池实现这个接口。这样应用程序可以方便的切换不同厂商的连接池!
4.代码实现
4.1 创建 MyDataSource2 并且实现 DataSource 接口
/**
* @author Aron.li
* @date 2021/1/25 1:16
*/
public class MyDataSource2 implements DataSource {
@Override
public Connection getConnection() throws SQLException {
return null;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
}
可以看到这个 DataSource
接口需要实现很多的方法,而我们自定义连接池的话不需要实现太多的功能,所以只要实现几个必要的方法即可。
4.2 创建无参构造器,用于创建 5 个初始连接
public class MyDataSource2 implements DataSource {
// 定义连接池
private LinkedList<Connection> connectionsPool = new LinkedList<>();
// 无参构造器,用于创建 5 个初始连接
public MyDataSource2() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lij*****************10";
Connection conn = DriverManager.getConnection(url, user, password);
//将连接添加到connectionPool中
connectionsPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4.3 重写 getConnection() 方法,提供获取连接
public class MyDataSource2 implements DataSource {
// 定义连接池
private LinkedList<Connection> connectionsPool = new LinkedList<>();
// 无参构造器,用于创建 5 个初始连接
public MyDataSource2() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lij*****************10";
Connection conn = DriverManager.getConnection(url, user, password);
//将连接添加到connectionPool中
connectionsPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public Connection getConnection() throws SQLException {
//从容器中获取连接
Connection conn = null;
if (connectionsPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectionsPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lij*****************10";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}
4.4 实现DataSource接口后,addBack()不能调用了.
那么我们应该怎么将线程加入回线程池呢?
这个时候我们只能默认使用连接的 close()
方法来关闭连接,可以考虑一下能否重写这个 close()
方法呢?
4.5 测试使用自定义连接池
/**
* @author Aron.li
* @date 2021/1/25 1:16
*/
public class MyDataSource2 implements DataSource {
// 定义连接池
private LinkedList<Connection> connectionsPool = new LinkedList<>();
// 无参构造器,用于创建 5 个初始连接
public MyDataSource2() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "L*****************0";
Connection conn = DriverManager.getConnection(url, user, password);
//将连接添加到connectionPool中
connectionsPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public Connection getConnection() throws SQLException {
//从容器中获取连接
Connection conn = null;
if (connectionsPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectionsPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "L*****************0";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
// 编写测试方法,创建多个连接测试使用如下:
@Test
public void test02() throws SQLException {
// 测试创建5个连接
MyDataSource2 dataSource = new MyDataSource2();
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
Connection connection3 = dataSource.getConnection();
Connection connection4 = dataSource.getConnection();
Connection connection5 = dataSource.getConnection();
// 超出连接池的数量
Connection connection6 = dataSource.getConnection();
// 关闭连接
connection1.close();
connection2.close();
connection3.close();
connection4.close();
connection5.close();
connection6.close();
}
}
那么下面我们要考虑一下如何将创建好的连接 加入回 连接池呢?
5.小结
5.1编写连接池遇到的问题
- 实现DataSource接口后,addBack()不能调用了.
- 能不能不引入新的api,直接调用之前的connection.close(),但是这个close不是关闭,是归还
5.2解决办法
- 继承
- 条件:可以控制父类, 最起码知道父类的名字
- 装饰者模式
- 增强类和被增强类实现的是同一个接口
- 增强类里面要拿到被增强类的引用
- 作用:改写已存在的类的某个方法或某些方法
- 条件:
- 动态代理
自定义连接池-终极版本
1.目标
在上面的代码中,存在一个无法将连接 connection 返回连接池的方法,下面我们可以使用装饰者模式改写connection
的close()
方法, 让connection
可以通过close()
方法 归还到连接池中。
2.自定义连接池终极版本
2.1分析
增强connection的close()方法, 其它的方法逻辑不改
- 创建WrapperConnection实现Connection
- 在WrapperConnection里面需要得到被增强的connection对象(通过构造方法传进去)
- 改写close()的逻辑, 变成归还
- 其它方法的逻辑, 还是调用被增强connection对象之前的逻辑
2.2 创建装饰者 WrapperConnection 类
2.2.1 创建 装饰者 WrapperConnection 类 实现 Connection 方法,用来重写 close() 方法,提供归还连接池
2.2.3 同时还要实现几个我们会用到的方法
2.2.4 装饰着类代码
/**
* @author Aron.li
* @date 2021/1/25 8:51
*/
public class WrapperConnection implements Connection {
private Connection connection; // 定义连接
private LinkedList<Connection> connectionPool; // 定义传入要操作的连接池
// 构造器: 用来传入 connection 连接 以及 连接池
public WrapperConnection(Connection connection, LinkedList<Connection> connectionPool) {
this.connection = connection;
this.connectionPool = connectionPool;
}
@Override
public void close() throws SQLException {
//将当前这个连接归还回原来那个连接池容器
connectionPool.addLast(this);
}
@Override
public Statement createStatement() throws SQLException {
return connection.createStatement();
}
@Override
public PreparedStatement prepareStatement(String sql) throws SQLException {
return connection.prepareStatement(sql);
}
//...其他方法省略
}
2.3 修改连接池 MyDataSource03, 使用装饰 WrapperConnection 类来创建连接
因为装饰后的连接被我们重写了 close()
方法,所以将连接池默认创建的连接为装饰后的连接。
代码如下:
public class MyDataSource3 implements DataSource {
// 定义连接池
private LinkedList<Connection> connectionsPool = new LinkedList<>();
// 无参构造器,用于创建 5 个初始连接
public MyDataSource3() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Li******************0";
//创建一个装饰后的连接
Connection conn = new WrapperConnection(DriverManager.getConnection(url, user, password),connectionsPool);
//将连接添加到connectionPool中
connectionsPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public Connection getConnection() throws SQLException {
//从容器中获取连接
Connection conn = null;
if (connectionsPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectionsPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Li******************0";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}
// 省略其他方法。。。
2.4 测试使用自定义连接池
2.5 代码
/**
* @author Aron.li
* @date 2021/1/25 1:16
*/
public class MyDataSource3 implements DataSource {
// 定义连接池
private LinkedList<Connection> connectionsPool = new LinkedList<>();
// 无参构造器,用于创建 5 个初始连接
public MyDataSource3() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lijw@********!610";
//创建一个装饰后的连接
Connection conn = new WrapperConnection(DriverManager.getConnection(url, user, password),connectionsPool);
//将连接添加到connectionPool中
connectionsPool.add(conn);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public Connection getConnection() throws SQLException {
//从容器中获取连接
Connection conn = null;
if (connectionsPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectionsPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lij********10";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return null;
}
@Override
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
@Override
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return null;
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
}
@Override
public int getLoginTimeout() throws SQLException {
return 0;
}
@Override
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
// 编写测试方法,创建多个连接测试使用如下:
@Test
public void test02() throws SQLException {
// 测试创建5个连接
MyDataSource3 dataSource = new MyDataSource3();
Connection connection1 = dataSource.getConnection();
Connection connection2 = dataSource.getConnection();
Connection connection3 = dataSource.getConnection();
Connection connection4 = dataSource.getConnection();
Connection connection5 = dataSource.getConnection();
// 超出连接池的数量
Connection connection6 = dataSource.getConnection();
// 关闭连接
connection1.close();
connection2.close();
connection3.close();
connection4.close();
connection5.close();
connection6.close();
}
}
3.小结
- 创建一个MyConnection实现Connection
- 在MyConnection得到被增强的connection对象
- 改写MyConnection里面的close()方法的逻辑为归还
- MyConnection里面的其它方法 调用被增强的connection对象之前的逻辑
- 在MyDataSource03的getConnection()方法里面 返回了myConnection
自定义连接池扩展版本-使用动态代理
在上面我们已经使用 装饰模式 解决了不同连接的不同 close() 问题,下面我们再来看看 动态代理。
使用动态代理创建Connection对象的代理对象,增强Connection的close方法
public class MyDataSource4 implements DataSource {
// 定义连接池
private LinkedList<Connection> connectionsPool = new LinkedList<>();
// 无参构造器,用于创建 5 个初始连接
public MyDataSource4() {
// 创建5个连接
for (int i = 0; i < 5; i++) {
try {
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lij******************0";
// 创建连接
Connection connection = DriverManager.getConnection(url, user, password);
ClassLoader classLoader = connection.getClass().getClassLoader();
//创建动态代理对象
Connection connectionProxy = (Connection) Proxy.newProxyInstance(classLoader, new Class[]{Connection.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//判断执行方法是否是close方法,如果是,则增强,如果不是则执行被代理者原本的方法
if (method.getName().equals("close")) {
//增强close
//将当前这个连接对象,添加到容器中
connectionsPool.addLast((Connection) proxy);
return null;
}
//不需要增强的方法,就执行原本的方法
return method.invoke(connection,args);
}
});
//将连接添加到connectionPool中
connectionsPool.add(connectionProxy);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
@Override
public Connection getConnection() throws SQLException {
//从容器中获取连接
Connection conn = null;
if (connectionsPool.size() > 0) {
//1.当连接池存在连接,那么则直接返回连接
conn = connectionsPool.removeFirst();
} else {
//2.当连接池不存在连接,则重新创建一个新的连接返回
//创建连接
String url = "jdbc:mysql://localhost:3306/test";
String user = "root";
String password = "Lij******************0";
try {
conn = DriverManager.getConnection(url, user, password);
} catch (SQLException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
return conn;
}