时间很快就到周末了,学习计划也已经进行了五天了,既然是周末的话,那当然要多学习一点知识,毕竟拥有这么充裕的时间。
今天的学习内容是数据库连接池。 那什么是数据库连接池,它有什么作用是我们首先会想到的问题。 数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是再重新建立一个。
应用程序直接获取连接的缺点: 用户每次请求都需要向数据库获得链接,而数据库创建连接通常需要消耗相对较大的资源,创建时间也较长。假设网站一天10万访问量,数据库服务器就需要创建10万次连接,极大的浪费数据库的资源,并且极易造成数据库服务器内存溢出、拓机。
缺点显而易见,应用程序在创建连接和销毁连接的时候是极其消耗资源的,而使用数据库连接池则能够优化程序性能。
连接池原理: 在服务器端一次性地创建多个连接,将多个连接保存在一个连接池对象中,当请求需要操作数据库时,不会为请求创建新的连接,而是直接从连接池中获得一个连接。当操作数据库结束,并不需要真正的去关闭连接,而是将连接放回到连接池中。
了解了数据库连接池的优点后,我们关心的是该如何去实现数据库连接池呢? 在Java中提供了javax.sql.Datasource接口用于实现数据库连接池。 老话说得好,光说不练假把式,只练不打无用功,现在我们就来写一个程序感受一下。
在MyEclipse中新建一个web项目,取名demo。 新建一个类,取名MyDataSource,然后实现DataSource接口,要实现的方法非常多,但是不用紧张,我们只关注两个方法。
代码语言:javascript复制public Connection getConnection() throws SQLException {
return null;
}
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
方便起见,我们只看无参的getConnection()方法。自定义的连接池需要有如下功能
- 一次性地创建多个连接
- 实现getConnection方法,从连接池获得一个连接
- 当用户使用连接后,提供方法将连接放回到连接池中 代码如下:
/**
* 自定义连接池
*
* 一次性地创建多个连接
*
* 实现getConnection方法,从连接池获得一个连接
*
* 当用户使用连接后,提供方法将连接放回到连接池中
*
* @author Administrator
*
*/
public class MyDataSource implements DataSource {
private LinkedList<Connection> dataSources = new LinkedList<Connection>();
public MyDataSource(){
//一次性创建10个连接
for(int i = 0;i < 10;i ){
try {
Connection connection = JDBCUtils.getConnection();
//将连接加入到连接池中
dataSources.add(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public PrintWriter getLogWriter() throws SQLException {
return null;
}
public void setLogWriter(PrintWriter out) throws SQLException {
}
public void setLoginTimeout(int seconds) throws SQLException {
}
public int getLoginTimeout() throws SQLException {
return 0;
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
public Connection getConnection() throws SQLException {
//取出连接池中的一个连接
Connection connection = dataSources.removeFirst();//删除第一个连接并返回
return connection;
}
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
}
代码中的JDBCUtils是我编写的一个工具类,在之前的博客中也都有提及,为了方便大家,我就再贴一次。
代码语言:javascript复制/**
* JDBC 工具类,抽取公共方法
*
* @author seawind
*
*/
public class JDBCUtils {
private static final String DRIVERCLASS;
private static final String URL;
private static final String USER;
private static final String PWD;
static {
ResourceBundle bundle = ResourceBundle.getBundle("dbconfig");
DRIVERCLASS = bundle.getString("DRIVERCLASS");
URL = bundle.getString("URL");
USER = bundle.getString("USER");
PWD = bundle.getString("PWD");
}
// 建立连接
public static Connection getConnection() throws Exception {
loadDriver();
return DriverManager.getConnection(URL, USER, PWD);
}
// 装载驱动
private static void loadDriver() throws ClassNotFoundException {
Class.forName(DRIVERCLASS);
}
// 释放资源
public static void release(ResultSet rs, Statement stmt, Connection conn) {
if (rs != null) {
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
rs = null;
}
release(stmt, conn);
}
public static void release(Statement stmt, Connection conn) {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
stmt = null;
}
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
conn = null;
}
}
}
现在新建一个测试类,取名MyDataSourceTest
代码语言:javascript复制public class MyDataSourceTest {
public static void main(String[] args) throws SQLException {
//创建连接池
MyDataSource dataSource = new MyDataSource();
//从连接池中获得一个连接
Connection connection = dataSource.getConnection();
//操作数据库
String sql = "select * from account";
PreparedStatement stmt = connection.prepareStatement(sql);
ResultSet rs = stmt.executeQuery();
while (rs.next()) {
System.out.println(rs.getString("name"));
}
JDBCUtils.release(stmt, connection);
}
}
运行测试类
控制台成功输出用户姓名。 但是这段程序是有问题的,因为JDBCUtils工具类中的release()方法会将连接关闭,而我们的想法是将连接归还到连接池而不是关闭它。实现方法有很多种,你可以在自定义连接池MyDataSource中添加一个方法用于归还连接。 重新修改MyDataSource类,代码如下
代码语言:javascript复制/**
* 自定义连接池
*
* 一次性地创建多个连接
*
* 实现getConnection方法,从连接池获得一个连接
*
* 当用户使用连接后,提供方法将连接放回到连接池中
*
* @author Administrator
*
*/
public class MyDataSource implements DataSource {
private LinkedList<Connection> dataSources = new LinkedList<Connection>();
public MyDataSource(){
//一次性创建10个连接
for(int i = 0;i < 10;i ){
try {
Connection connection = JDBCUtils.getConnection();
//将连接加入到连接池中
dataSources.add(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public PrintWriter getLogWriter() throws SQLException {
return null;
}
public void setLogWriter(PrintWriter out) throws SQLException {
}
public void setLoginTimeout(int seconds) throws SQLException {
}
public int getLoginTimeout() throws SQLException {
return 0;
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
/**
* 添加一个方法,用于归还连接
* @param conn
*/
public void releaseConnection(Connection conn){
dataSources.add(conn);
System.out.println("将连放回到连接池中,数量" dataSources.size());
}
public Connection getConnection() throws SQLException {
//取出连接池中的一个连接
Connection connection = dataSources.removeFirst();//删除第一个连接并返回
System.out.println("取出一个连接,剩余" dataSources.size() "个连接");
return connection;
}
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
}
测试类中最后就应该调用你刚刚添加的方法,运行测试类
结果很明显,目的达到了。 这样就实现了一个简易的数据库连接池,但该程序其实有一个很不好的地方,因为如果想要将连接放回连接池而不是关闭它,就得调用在MyDataSource类中自己添加的方法,而很多人或许已经习惯了JDBC的编程,会很习惯地去调用close()方法,而一旦调用了close()方法,连接就被关闭了,不会放回连接池了。所以你就需要通知用户去主动使用你添加的API,这种方法其实是不可取的。那有什么办法能够让调用者随着自己的编程习惯去调用close()方法的同时,还能够将连接放回连接池而不是关闭它呢? 我们可以去修改close()方法原来的逻辑,怎么修改呢? 在Java中有三种方法可以增强原有的方法
- 类继承 、方法覆盖 必须控制对象创建,才能使用该方式
- 装饰者模式方法加强 必须和目标对象实现相同接口或继续相同父类,特殊构造器(传入被包装对象)
- 动态代理
有关方法增强的问题,可以参考我的这篇博客。 理解Java方法增强 熟悉方法增强的小伙伴们可以忽略哈。(手动滑稽)
我们使用动态代理来对Connection接口的run()方法进行增强。 修改MyDataSource类中的代码
代码语言:javascript复制/**
* 自定义连接池
*
* 一次性地创建多个连接
*
* 实现getConnection方法,从连接池获得一个连接
*
* 当用户使用连接后,提供方法将连接放回到连接池中
*
* @author Administrator
*
*/
public class MyDataSource implements DataSource {
private LinkedList<Connection> dataSources = new LinkedList<Connection>();
public MyDataSource() {
// 一次性创建10个连接
for (int i = 0; i < 10; i ) {
try {
Connection connection = JDBCUtils.getConnection();
// 将连接加入到连接池中
dataSources.add(connection);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public PrintWriter getLogWriter() throws SQLException {
return null;
}
public void setLogWriter(PrintWriter out) throws SQLException {
}
public void setLoginTimeout(int seconds) throws SQLException {
}
public int getLoginTimeout() throws SQLException {
return 0;
}
public Logger getParentLogger() throws SQLFeatureNotSupportedException {
return null;
}
public <T> T unwrap(Class<T> iface) throws SQLException {
return null;
}
public boolean isWrapperFor(Class<?> iface) throws SQLException {
return false;
}
/**
* 添加一个方法,用于归还连接
*
* @param conn
*/
public void releaseConnection(Connection conn) {
dataSources.add(conn);
System.out.println("将连放回到连接池中,数量" dataSources.size());
}
public Connection getConnection() throws SQLException {
// 取出连接池中的一个连接
final Connection connection = dataSources.removeFirst();// 删除第一个连接并返回
System.out.println("取出一个连接,剩余" dataSources.size() "个连接");
// 将目标connection对象进行增强
Connection connProxy = (Connection) Proxy.newProxyInstance(connection
.getClass().getClassLoader(), connection.getClass()
.getInterfaces(), new InvocationHandler() {
//执行代理对象的任何方法都将执行invoke方法
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable {
//只需要增强close方法
if(method.getName().equals("close")){
//需要增强的方法
//不将连接真正关闭,将连接放回连接池
releaseConnection(connection);
return null;
}else{
//不需要增强的方法,保持方法原有的功能即可
return method.invoke(connection, args);
}
}
});
return connProxy;
}
public Connection getConnection(String username, String password)
throws SQLException {
return null;
}
}
回到测试类,此时调用Connection接口中的close()方法,然后执行
方法成功增强了。调用者在调用close()方法后并不会把连接关闭了,而是放回连接池中。由此我们的想法便实现了。