这段时间,正在学Hibernate,在学习的过程中,回去复习了一下JDBC,收获颇大。其实,很早以前(大二),就接触JDBC了,只是当时觉得很简单,大概是那时对于软件开发的认识比较肤浅吧!总认为,能够通过别人提供的高级工具,例如:Struts、Hibernate、Spring开发出一个东西出来,就是很厉害了。所以,在那时候,感觉能够熟练使用JDBC的一些API,就算是精通JDBC了。
现在才觉得,自己当时的想法是幼稚的,原谅我的年轻,我追求进步。
今天,看了一下DBCP(Apache提供的通过连接池获取Connection的一个项目),然后用MyEclipse的debug功能,看了下使用DBCP的API获取Connection的执行能够过程。接着,花了几个小时的时间,自己写了一个数据源,与大家分享,倘若在设计上有什么不合理的地方,还请大家给鄙人指出。鄙人的进步需要你们。
先谈下“为什么要使用连接池(数据源)”?
我们知道,在我们使用JDBC操作数据库的时候,过程一般都是如此的:注册数据库驱动-->取得连接(Connection)-->取得Statement或者PreparedStatement-->(可能需要)取得ResultSet,而我们知道,在这一操作中,最耗时的操作是“取得Connection”这个过程。
所以,我们在想?既然Connection创建的成本这么高。为什么不去复用它们呢!用完了就关闭,太浪费了。Connection就像一座桥,而PreparedStatement或Statement对象就像一辆车,ResultSet就像一匹货物。货物运完了,就把桥拆了,多浪费呀!所以,我们将这座桥(Connection对象)保留起来。用什么方式保留呢?一种比较容易实现的方式,当Connection对象不要用时,将Connection对象放在一个集合里,而不是关闭了。可以是ArrayList,可以是LinkedList(下面会强调两者的区别)。
好,下面就是具体的实现过程。
- 首先,需要有一个配置文件,配置文件是必须的。
<?xml version="1.0" encoding="UTF-8"?> <datasource-configuraction> <!--连接设置 --> <driver-name>com.mysql.jdbc.Driver</driver-name> <url>jdbc:mysql://localhost:3306/jdbc</url> <username>root</username> <password>root</password> <!-- 初始化连接数 --> <inital-size>10</inital-size> <!-- 最大连接数 --> <max-active>20</max-active> <!-- 每个连接最大使用次数 --> <max-used>5</max-used> </datasource-configuraction>
该配置文件,配置了以下信息:
- 数据库连接信息:例如驱动,url,username,password
- 初始化连接数:当数据源(下面的MyDataSource)实例化时,创建的Connection的个数
- 最大连接数:我们知道,WEB环境,一般都是多线程环境的。由于Connection是非线程安全的,所以要确保每个线程拿到的都是不同的Connection。那假如有100个线程同时访问数据库,很显然,连接池里的Connection是不够用的,所以必须额外创建。而数据库所能承受的Connection个数是有限的,我们为了保证数据库的性能,不能让Connection无休止地创建下去。
- 每个连接最大使用次数:见下文
- 接着,有了配置文件,就要去读取了,我使用的是dom4j读取
package com.langgou.jdbc.datasource.util;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;
/**
* 读取数据源配置信息
*
* @author <a href="fumingfu1990@gmail.com">胡铭福</a>
*
*/
public final class DataSourceCfgReader {
private static Document doc;
private static DataSourceInfo dateSourceInfo;
private DataSourceCfgReader() {
}
static {
try {
doc = new SAXReader().read(DataSourceCfgReader.class
.getClassLoader().getResource("dataSourceCfg.xml"));
setDataSourceInfo(doc);
} catch (DocumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//设置数据源配置信息
private static void setDataSourceInfo(Document doc) {
Element driverNameElt = (Element) doc
.selectObject("/datasource-configuraction/driver-name");
Element urlElt = (Element) doc
.selectObject("/datasource-configuraction/url");
Element userNameElt = (Element) doc
.selectObject("/datasource-configuraction/username");
Element passwordElt = (Element) doc
.selectObject("/datasource-configuraction/password");
Element initalSizeElt = (Element) doc
.selectObject("/datasource-configuraction/inital-size");
Element maxActiveElt = (Element) doc
.selectObject("/datasource-configuraction/max-active");
Element maxUsedElt = (Element) doc
.selectObject("/datasource-configuraction/max-used");
dateSourceInfo = new DataSourceInfo();
dateSourceInfo.setDriverName(driverNameElt.getText());
dateSourceInfo.setUrl(urlElt.getText());
dateSourceInfo.setUserName(userNameElt.getText());
dateSourceInfo.setPassword(passwordElt.getText());
dateSourceInfo.setInitalSize(Integer.parseInt(initalSizeElt.getText()));
dateSourceInfo.setMaxActive(Integer.parseInt(maxActiveElt.getText()));
dateSourceInfo.setMaxUsed(Integer.parseInt(maxUsedElt.getText()));
}
//取得数据源配置信息
public static DataSourceInfo getDataSourceInfo() {
return dateSourceInfo;
}
}
- 读取出来的信息,得用一个JavaBean来存放,这样会比较合理一点。因为类和类之间,一般都是通过JavaBean作数据通信的。
package com.langgou.jdbc.datasource.util;
/**
* 保存数据源配置信息
* @author <a href="fumingfu1990@gmail.com">胡铭福</a>
*
*/
public class DataSourceInfo {
/**
* 连接设置
*/
private String driverName;
private String url;
private String userName;
private String password;
/**
* 初始化的Connection个数
*/
private int initalSize;
/**
* 已创建Connection的最大个数
*/
private int maxActive;
/**
* Connection的最大使用次数
*/
private int maxUsed;
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getInitalSize() {
return initalSize;
}
public void setInitalSize(int initalSize) {
this.initalSize = initalSize;
}
public int getMaxActive() {
return maxActive;
}
public void setMaxActive(int maxActive) {
this.maxActive = maxActive;
}
public int getMaxUsed() {
return maxUsed;
}
public void setMaxUsed(int maxUsed) {
this.maxUsed = maxUsed;
}
}
- 现在,配置信息读取出来了。接下来,就是重头戏了,也就是数据源的实现.(程序的注释已经写的很详细了,这里就不再详细解释了)
package com.langgou.jdbc.datasource;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.LinkedList;
import com.langgou.jdbc.datasource.util.DataSourceCfgReader;
import com.langgou.jdbc.datasource.util.DataSourceInfo;
public class MyDataSource {
/**
* 数据源配置文件中的所有信息,当MyDateSource一被加载入虚拟机,就读取所有配置文件信息
*/
protected static DataSourceInfo dataSourceInfo = DataSourceCfgReader
.getDataSourceInfo();
private final static String PROP_DRIVERCLASSNAME = dataSourceInfo
.getDriverName();
private final static String PROP_URL = dataSourceInfo.getUrl();
private final static String PROP_USERNAME = dataSourceInfo.getUserName();
private final static String PROP_PASSWORD = dataSourceInfo.getPassword();
/**
* 初始化的Connection个数
*/
private final static int PROP_INITIALSIZE = dataSourceInfo.getInitalSize();
/**
* 最大的Connection个数
*/
private final static int PROP_MAXACTIVE = dataSourceInfo.getMaxActive();
/**
* 每个Connection的最大使用次数
*/
private final static int PROP_MAXUSED = dataSourceInfo.getMaxUsed();
/**
* 记录已创建的Connection个数
*/
protected int currentCount = 0;
/**
* 连接池,将connectionPool设成protected的,只允许包内访问。外部只能通过调用getConnection()方法取得Connection
*/
protected LinkedList<Connection> connectionPool = new LinkedList<Connection>();
/**
* 当MyDataSource被加载到虚拟机,就注册数据库驱动
*/
static {
try {
Class.forName(PROP_DRIVERCLASSNAME);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* 当MyDataSource被实例化,就创建一批Connection,放入连接池里 构造方法
*/
public MyDataSource() {
try {
for (int i = 0; i < PROP_INITIALSIZE; i++) {
this.connectionPool.addLast(this.createConnection());
this.currentCount++;
}
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
*
* @author <a href="fumingfu1990@gmail.com">胡铭福</a> 功能:从连接池里取得Connection
* @return
*/
public Connection getConnection() throws SQLException {
// Connection属于非线程安全的,要确保每个线程拿到的都是不相同的Connection
synchronized (connectionPool) {
// 判断连接池里是否还有Connection
if (this.connectionPool.size() > 0) {
// 如果连接池里还有Connection,则直接从连接池中取出Connection
return connectionPool.removeFirst();
} else if (this.currentCount < PROP_MAXACTIVE) {
// 判断当前的Connection个数是否超过Connection个数的上限
// 因为数据库所能承受的Connection个数是有限的,不能让其无限创建下去
this.currentCount++;
try {
// 创建一个新的Connection
return this.createConnection();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
throw new SQLException("已经没有连接啦!");
}
}
/**
*
* @author <a href="fumingfu1990@gmail.com">胡铭福</a> 功能:释放Connection
* @param conn
*/
public void free(Connection conn) {
// 为了达到Connection的复用,不能将Connection关闭,要放回连接池中
this.connectionPool.add(conn);
}
/**
*
* @author <a href="fumingfu1990@gmail.com">胡铭福</a> 功能:创建Connection
* @return
* @throws SQLException
* 问题:创建Connection的时候,采用ThreadLocal封装,是否有效?
*/
private Connection createConnection() throws SQLException {
// //采用ThreadLocal封装Connection,确保每个线程只对应一个Connection
// ThreadLocal<Connection> connectionHolder = new
// ThreadLocal<Connection>();
Connection conn = null;
try {
// 取得与当前线程绑定的Connection
// conn = connectionHolder.get();
// 如果没有与当前线程绑定的Connection
// if(conn==null){
// 创建Connection
conn = DriverManager.getConnection(PROP_URL, PROP_USERNAME,PROP_PASSWORD);
// 将创建的Connection与当前线程绑定
// connectionHolder.set(conn);
// }
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
return conn;
}
// /**
// *
// * @author <a href="fumingfu1990@gmail.com">胡铭福</a>
// * 功能:测试程序
// * @param args
// */
// public static void main(String[] args) {
// MyDataSource myDataSource = new MyDataSource();
//
// for(int i=0;i<5;i++){
// try {
// System.out.println(myDataSource.getConnection());
// } catch (SQLException e) {
// // TODO Auto-generated catch block
// e.printStackTrace();
// }
// }
// }
}
存在的问题:由于当用户显式的调用Connection对象的close()方法时,默认会将Connection对象关闭,而不是返回连接池中,从而不能达到Connection的复用。所以要采取一种机制,改写掉Connection的close()方法默认的行为。
解决方案:
一般采用采用JDK动态代理,拦截所有Connection对象的close()方法,由代理类实现。将默认的关闭Connection对象的行为改为将Connection对象返回连接池,从而达到Connection对象的复用。而除了close()方法,外的其余方法,都由真实对象(realConnection对象)实现.
------------------------------------------------------------------这是一条分割线----------------------------------------------------------------------------------------------
由于时间关系,采用代理类包装Connection,这篇文章就不再介绍了。下篇文章,会详细介绍,并实现类似于DBCP包装Connection的方法。
由于是“处女贴”,写的不好的地方,请谅解。