当前位置: 首页 >> 程序设计 >> Java >> Java jdbc数据库连接池总结
Java jdbc数据库连接池总结
1. 引言
近年来,随着Internet/Intranet建网技术的飞速发展和在世界范围内的迅速普及,计算机
应用程序已从传统的桌面应用转到Web应用。基于B/S(Browser/Server)架构的3层开发模式逐渐取代C/S(Client/Server)架构的开发模式,成为开发企业级应用和电子商务普遍采用的技术。在Web应用开发的早期,主要使用的技术是CGI﹑ASP﹑PHP等。之后,Sun公司推出了基于Java语言的Servlet+Jsp+JavaBean技术。相比传统的开发技术,它具有跨平台﹑安全﹑有效﹑可移植等特性,这使其更便于使用和开发。
Java应用程序访问数据库的基本原理
在Java语言中,JDBC(Java DataBase Connection)是应用程序与数据库沟通的桥梁,
即Java语言通过JDBC技术访问数据库。JDBC是一种“开放”的方案,它为数据库应用开发人员﹑数据库前台工具开发人员提供了一种标准的应用程序设计接口,使开发人员可以用纯Java语言编写完整的数据库应用程序。JDBC提供两种API,分别是面向开发人员的API和面向底层的JDBC驱动程序API,底层主要通过直接的JDBC驱动和JDBC-ODBC桥驱动实现与数据库的连接。
一般来说,Java应用程序访问数据库的过程(如图1所示)是:
①装载数据库驱动程序;
②通过JDBC建立数据库连接;
③访问数据库,执行SQL语句;
④断开数据库连接。
图1 Java数据库访问机制
JDBC作为一种数据库访问技术,具有简单易用的优点。但使用这种模式进行Web应用
程序开发,存在很多问题:首先,每一次Web请求都要建立一次数据库连接。建立连接是一个费时的活动,每次都得花费0.05s~1s的时间,而且系统还要分配内存资源。这个时间对于一次或几次数据库操作,或许感觉不出系统有多大的开销。可是对于现在的Web应用,尤其是大型电子商务网站,同时有几百人甚至几千人在线是很正常的事。在这种情况下,频繁的进行数据库连接操作势必占用很多的系统资源,网站的响应速度必定下降,严重的甚至会造成服务器的崩溃。不是危言耸听,这就是制约某些电子商务网站发展的技术瓶颈问题。其次,对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将不得不重启数据库。还有,这种开发不能控制被创建的连接对象数,系统资源会被毫无顾及的分配出去,如连接过多,也可能导致内存泄漏,服务器崩溃。
数据库连接池(connection pool)的工作原理
1、基本概念及原理
由上面的分析可以看出,问题的根源就在于对数据库连接资源的低效管理。我们知道,
对于共享资源,有一个很著名的设计模式:资源池(Resource Pool)。该模式正是为了解决资源的频繁分配﹑释放所造成的问题。为解决上述问题,可以采用数据库连接池技术。数据库连接池的基本思想就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接。更为重要的是我们可以通过连接池的管理机制监视数据库的连接的数量﹑使用情况,为系统开发﹑测试及性能调整提供依据。连接池的基本工作原理见下图2。
图2 连接池的基本工作原理
2、服务器自带的连接池
JDBC的API中没有提供连接池的方法。一些大型的WEB应用服务器如BEA的WebLogic和IBM的WebSphere等提供了连接池的机制,但是必须有其第三方的专用类方法支持连接池的用法。
连接池关键问题分析
1、并发问题
为了使连接管理服务具有最大的通用性,必须考虑多线程环境,即并发问题。这个问题相对比较好解决,因为Java语言自身提供了对并发管理的支持,使用synchronized关键字即可确保线程是同步的。使用方法为直接在类方法前面加上synchronized关键字,如:
public synchronized Connection getConnection()
2、多数据库服务器和多用户
对于大型的企业级应用,常常需要同时连接不同的数据库(如连接Oracle和Sybase)。如何连接不同的数据库呢?我们采用的策略是:设计一个符合单例模式的连接池管理类,在连接池管理类的唯一实例被创建时读取一个资源文件,其中资源文件中存放着多个数据库的url地址(<poolName.url>)﹑用户名(<poolName.user>)﹑密码(<poolName.password>)等信息。如tx.url=192.168.1.123:5000/tx_it,tx.user=cyl,tx.password=123456。根据资源文件提供的信息,创建多个连接池类的实例,每一个实例都是一个特定数据库的连接池。连接池管理类实例为每个连接池实例取一个名字,通过不同的名字来管理不同的连接池。
对于同一个数据库有多个用户使用不同的名称和密码访问的情况,也可以通过资源文件处理,即在资源文件中设置多个具有相同url地址,但具有不同用户名和密码的数据库连接信息。
3、事务处理
我们知道,事务具有原子性,此时要求对数据库的操作符合“ALL-ALL-NOTHING”原则,即对于一组SQL语句要么全做,要么全不做。
在Java语言中,Connection类本身提供了对事务的支持,可以通过设置Connection的AutoCommit属性为false,然后显式的调用commit或rollback方法来实现。但要高效的进行Connection复用,就必须提供相应的事务支持机制。可采用每一个事务独占一个连接来实现,这种方法可以大大降低事务管理的复杂性。
4、连接池的分配与释放
连接池的分配与释放,对系统的性能有很大的影响。合理的分配与释放,可以提高连接的复用度,从而降低建立新连接的开销,同时还可以加快用户的访问速度。
对于连接的管理可使用空闲池。即把已经创建但尚未分配出去的连接按创建时间存放到一个空闲池中。每当用户请求一个连接时,系统首先检查空闲池内有没有空闲连接。如果有就把建立时间最长(通过容器的顺序存放实现)的那个连接分配给他(实际是先做连接是否有效的判断,如果可用就分配给用户,如不可用就把这个连接从空闲池删掉,重新检测空闲池是否还有连接);如果没有则检查当前所开连接池是否达到连接池所允许的最大连接数(maxConn),如果没有达到,就新建一个连接,如果已经达到,就等待一定的时间(timeout)。如果在等待的时间内有连接被释放出来就可以把这个连接分配给等待的用户,如果等待时间超过预定时间timeout,则返回空值(null)。系统对已经分配出去正在使用的连接只做计数,当使用完后再返还给空闲池。对于空闲连接的状态,可开辟专门的线程定时检测,这样会花费一定的系统开销,但可以保证较快的响应速度。也可采取不开辟专门线程,只是在分配前检测的方法。
5、连接池的配置与维护
连接池中到底应该放置多少连接,才能使系统的性能最佳?系统可采取设置最小连接数(minConn)和最大连接数(maxConn)来控制连接池中的连接。最小连接数是系统启动时连接池所创建的连接数。如果创建过多,则系统启动就慢,但创建后系统的响应速度会很快;如果创建过少,则系统启动的很快,响应起来却慢。这样,可以在开发时,设置较小的最小连接数,开发起来会快,而在系统实际使用时设置较大的,因为这样对访问客户来说速度会快些。最大连接数是连接池中允许连接的最大数目,具体设置多少,要看系统的访问量,可通过反复测试,找到最佳点。
如何确保连接池中的最小连接数呢?有动态和静态两种策略。动态即每隔一定时间就对连接池进行检测,如果发现连接数量小于最小连接数,则补充相应数量的新连接,以保证连接池的正常运转。静态是发现空闲连接不够时再去检查。
连接池的实现
1、连接池模型
本文讨论的连接池包括一个连接池类(DBConnectionPool)和一个连接池管理类(DBConnetionPoolManager)和一个配置文件操作类(ParseDSConfig)。连接池类是对某一数据库所有连接的“缓冲池”,主要实现以下功能:①从连接池获取或创建可用连接;②使用完毕之后,把连接返还给连接池;③在系统关闭前,断开所有连接并释放连接占用的系统资源;④还能够处理无效连接(原来登记为可用的连接,由于某种原因不再可用,如超时,通讯问题),并能够限制连接池中的连接总数不低于某个预定值和不超过某个预定值。(5)当多数据库时,且数据库是动态增加的话,将会加到配置文件中。
连接池管理类是连接池类的外覆类(wrapper),符合单例模式,即系统中只能有一个连接池管理类的实例。其主要用于对多个连接池对象的管理,具有以下功能:①装载并注册特定数据库的JDBC驱动程序;②根据属性文件给定的信息,创建连接池对象;③为方便管理多个连接池对象,为每一个连接池对象取一个名字,实现连接池名字与其实例之间的映射;④跟踪客户使用连接情况,以便需要是关闭连接释放资源。连接池管理类的引入主要是为了方便对多个连接池的使用和管理,如系统需要连接不同的数据库,或连接相同的数据库但由于安全性问题,需要不同的用户使用不同的名称和密码。
2、连接池实现(经过本人改版,可以适用多数据库类型的应用以及一种数据库类型多个数据库且数据 库的数量可以动态增加的应用程序)
1),DBConnectionPool.java 数据库连接池类
2),DBConnectionManager .java 数据库管理类
3),DSConfigBean .java 单个数据库连接信息Bean
4),ParseDSConfig.java 操作多(这个'多'包括不同的数据库和同一种数据库有多个数据库)
数据 配置文件xml
5),ds.config.xml 数据库配置文件xml
----------------------------------------------------------
/**
* 数据库连接池类
*/
package com.chunkyo.db;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Timer;
/**
* @author chenyanlin
*
*/
public class DBConnectionPool implements TimerListener {
private Connection con=null;
private int inUsed=0; //使用的连接数
private ArrayList freeConnections = new ArrayList();//容器,空闲连接
private int minConn; //最小连接数
private int maxConn; //最大连接
private String name; //连接池名字
private String password; //密码
private String url; //数据库连接地址
private String driver; //驱动
private String user; //用户名
public Timer timer; //定时
/**
*
*/
public DBConnectionPool() {
// TODO Auto-generated constructor stub
}
/**
* 创建连接池
* @param driver
* @param name
* @param URL
* @param user
* @param password
* @param maxConn
*/
public DBConnectionPool(String name, String driver,String URL, String user, String password, int maxConn)
{
this.name=name;
this.driver=driver;
this.url=URL;
this.user=user;
this.password=password;
this.maxConn=maxConn;
}
/**
* 用完,释放连接
* @param con
*/
public synchronized void freeConnection(Connection con)
{
this.freeConnections.add(con);//添加到空闲连接的末尾
this.inUsed--;
}
/**
* timeout 根据timeout得到连接
* @param timeout
* @return
*/
public synchronized Connection getConnection(long timeout)
{
Connection con=null;
if(this.freeConnections.size()>0)
{
con=(Connection)this.freeConnections.get(0);
if(con==null)con=getConnection(timeout); //继续获得连接
}
else
{
con=newConnection(); //新建连接
}
if(this.maxConn==0||this.maxConn<this.inUsed)
{
con=null;//达到最大连接数,暂时不能获得连接了。
}
if(con!=null)
{
this.inUsed++;
}
return con;
}
/**
*
* 从连接池里得到连接
* @return
*/
public synchronized Connection getConnection()
{
Connection con=null;
if(this.freeConnections.size()>0)
{
con=(Connection)this.freeConnections.get(0);
this.freeConnections.remove(0);//如果连接分配出去了,就从空闲连接里删除
if(con==null)con=getConnection(); //继续获得连接
}
else
{
con=newConnection(); //新建连接
}
if(this.maxConn==0||this.maxConn<this.inUsed)
{
con=null;//等待 超过最大连接时
}
if(con!=null)
{
this.inUsed++;
System.out.println("得到 "+this.name+" 的连接,现有"+inUsed+"个连接在使用!");
}
return con;
}
/**
*释放全部连接
*
*/
public synchronized void release()
{
Iterator allConns=this.freeConnections.iterator();
while(allConns.hasNext())
{
Connection con=(Connection)allConns.next();
try
{
con.close();
}
catch(SQLException e)
{
e.printStackTrace();
}
}
this.freeConnections.clear();
}
/**
* 创建新连接
* @return
*/
private Connection newConnection()
{
try {
Class.forName(driver);
con=DriverManager.getConnection(url, user, password);
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
System.out.println("sorry can't find db driver!");
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
System.out.println("sorry can't create Connection!");
}
return con;
}
/**
* 定时处理函数
*/
public synchronized void TimerEvent()
{
//暂时还没有实现以后会加上的
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
/**
* @return the driver
*/
public String getDriver() {
return driver;
}
/**
* @param driver the driver to set
*/
public void setDriver(String driver) {
this.driver = driver;
}
/**
* @return the maxConn
*/
public int getMaxConn() {
return maxConn;
}
/**
* @param maxConn the maxConn to set
*/
public void setMaxConn(int maxConn) {
this.maxConn = maxConn;
}
/**
* @return the minConn
*/
public int getMinConn() {
return minConn;
}
/**
* @param minConn the minConn to set
*/
public void setMinConn(int minConn) {
this.minConn = minConn;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the password
*/
public String getPassword() {
return password;
}
/**
* @param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* @return the url
*/
public String getUrl() {
return url;
}
/**
* @param url the url to set
*/
public void setUrl(String url) {
this.url = url;
}
/**
* @return the user
*/
public String getUser() {
return user;
}
/**
* @param user the user to set
*/
public void setUser(String user) {
this.user = user;
}
}
DBConnectionManager .java
------------------------------------------
/**
* 数据库连接池管理类
*/
package com.chunkyo.db;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.Properties;
import java.util.Vector;
import com.chunkyo.db.ParseDSConfig;
import com.chunkyo.db.DSConfigBean;
import com.chunkyo.db.DBConnectionPool;
/**
* @author chenyanlin
*
*/
public class DBConnectionManager {
static private DBConnectionManager instance;//唯一数据库连接池管理实例类
static private int clients; //客户连接数
private Vector drivers = new Vector();//驱动信息
private Hashtable pools=new Hashtable();//连接池
/**
* 实例化管理类
*/
public DBConnectionManager() {
// TODO Auto-generated constructor stub
this.init();
}
/**
* 得到唯一实例管理类
* @return
*/
static synchronized public DBConnectionManager getInstance()
{
if(instance==null)
{
instance=new DBConnectionManager();
}
return instance;
}
/**
* 释放连接
* @param name
* @param con
*/
public void freeConnection(String name, Connection con)
{
DBConnectionPool pool=(DBConnectionPool)pools.get(name);//根据关键名字得到连接池
if(pool!=null)
pool.freeConnection(con);//释放连接
}
/**
* 得到一个连接根据连接池的名字name
* @param name
* @return
*/
public Connection getConnection(String name)
{
DBConnectionPool pool=null;
Connection con=null;
pool=(DBConnectionPool)pools.get(name);//从名字中获取连接池
con=pool.getConnection();//从选定的连接池中获得连接
if(con!=null)
System.out.println("得到连接");
return con;
}
/**
* 得到一个连接,根据连接池的名字和等待时间
* @param name
* @param time
* @return
*/
public Connection getConnection(String name, long timeout)
{
DBConnectionPool pool=null;
Connection con=null;
pool=(DBConnectionPool)pools.get(name);//从名字中获取连接池
con=pool.getConnection(timeout);//从选定的连接池中获得连接
System.out.println("得到连接");
return con;
}
/**
* 释放所有连接
*/
public synchronized void release()
{
Enumeration allpools=pools.elements();
while(allpools.hasMoreElements())
{
DBConnectionPool pool=(DBConnectionPool)allpools.nextElement();
if(pool!=null)pool.release();
}
pools.clear();
}
/**
* 创建连接池
* @param props
*/
private void createPools(DSConfigBean dsb)
{
DBConnectionPool dbpool=new DBConnectionPool();
dbpool.setName(dsb.getName());
dbpool.setDriver(dsb.getDriver());
dbpool.setUrl(dsb.getUrl());
dbpool.setUser(dsb.getUsername());
dbpool.setPassword(dsb.getPassword());
dbpool.setMaxConn(dsb.getMaxconn());
System.out.println("ioio:"+dsb.getMaxconn());
pools.put(dsb.getName(), dbpool);
}
/**
* 初始化连接池的参数
*/
private void init()
{
//加载驱动程序
this.loadDrivers();
//创建连接池
Iterator alldriver=drivers.iterator();
while(alldriver.hasNext())
{
this.createPools((DSConfigBean)alldriver.next());
System.out.println("创建连接池");
}
System.out.println("创建连接池完毕");
}
/**
* 加载驱动程序
* @param props
*/
private void loadDrivers()
{
ParseDSConfig pd=new ParseDSConfig();
//读取数据库配置文件
drivers=pd.readConfigInfo("ds.config.xml");
System.out.println("加载驱动程序");
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
}
----------------------------------------
DSConfigBean.java
----------------------------------------
/**
* 配置文件Bean类
*/
package com.chunkyo.db;
/**
* @author chenyanlin
*
*/
public class DSConfigBean {
private String type =""; //数据库类型
private String name =""; //连接池名字
private String driver =""; //数据库驱动
private String url =""; //数据库url
private String username =""; //用户名
private String password =""; //密码
private int maxconn =0; //最大连接数
/**
*
*/
public DSConfigBean() {
// TODO Auto-generated constructor stub
}
/**
* @param args
*/
public static void main(String[] args) {
// TODO Auto-generated method stub
}
/**
* @return the driver
*/
public String getDriver() {
return driver;
}
/**
* @param driver the driver to set
*/
public void setDriver(String driver) {
this.driver = driver;
}
/**
* @return the maxconn
*/
public int getMaxconn() {
return maxconn;
}
/**
* @param maxconn the maxconn to set
*/
public void setMaxconn(int maxconn) {
this.maxconn = maxconn;
}
/**
* @return the name
*/
public String getName() {
return name;
}
/**
* @param name the name to set
*/
public void setName(String name) {
this.name = name;
}
/**
* @return the password
*/
public String getPassword() {
return password;
}
/**
* @param password the password to set
*/
public void setPassword(String password) {
this.password = password;
}
/**
* @return the type
*/
public String getType() {
return type;
}
/**
* @param type the type to set
*/
public void setType(String type) {
this.type = type;
}
/**
* @return the url
*/
public String getUrl() {
return url;
}
/**
* @param url the url to set
*/
public void setUrl(String url) {
this.url = url;
}
/**
* @return the username
*/
public String getUsername() {
return username;
}
/**
* @param username the username to set
*/
public void setUsername(String username) {
this.username = username;
}
}
ParseDSConfig.java
-----------------------------------------------------
/**
* 操作配置文件类 读 写 修改 删除等操作
*/
package com.chunkyo.db;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
import java.util.Vector;
import java.util.Iterator;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
/**
* @author chenyanlin
*
*/
public class ParseDSConfig {
/**
* 构造函数
*/
public ParseDSConfig() {
// TODO Auto-generated constructor stub
}
/**
* 读取xml配置文件
* @param path
* @return
*/
public Vector readConfigInfo(String path)
{
String rpath=this.getClass().getResource("").getPath().substring(1)+path;
Vector dsConfig=null;
FileInputStream fi = null;
try
{
fi=new FileInputStream(rpath);//读取路径文件
dsConfig=new Vector();
SAXBuilder sb=new SAXBuilder();
Document doc=sb.build(fi);
Element root=doc.getRootElement();
List pools=root.getChildren();
Element pool=null;
Iterator allPool=pools.iterator();
while(allPool.hasNext())
{
pool=(Element)allPool.next();
DSConfigBean dscBean=new DSConfigBean();
dscBean.setType(pool.getChild("type").getText());
dscBean.setName(pool.getChild("name").getText());
System.out.println(dscBean.getName());
dscBean.setDriver(pool.getChild("driver").getText());
dscBean.setUrl(pool.getChild("url").getText());
dscBean.setUsername(pool.getChild("username").getText());
dscBean.setPassword(pool.getChild("password").getText());
dscBean.setMaxconn(Integer.parseInt(pool.getChild("maxconn").getText()));
dsConfig.add(dscBean);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally
{
try {
fi.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
return dsConfig;
}
/**
*修改配置文件 没时间写 过段时间再贴上去 其实一样的
*/
public void modifyConfigInfo(String path,DSConfigBean dsb) throws Exception
{
String rpath=this.getClass().getResource("").getPath().substring(1)+path;
FileInputStream fi=null; //读出
FileOutputStream fo=null; //写入
}
/**
*增加配置文件
*
*/
public void addConfigInfo(String path,DSConfigBean dsb)
{
String rpath=this.getClass().getResource("").getPath().substring(1)+path;
FileInputStream fi=null;
FileOutputStream fo=null;
try
{
fi=new FileInputStream(rpath);//读取xml流
SAXBuilder sb=new SAXBuilder();
Document doc=sb.build(fi); //得到xml
Element root=doc.getRootElement();
List pools=root.getChildren();//得到xml子树
Element newpool=new Element("pool"); //创建新连接池
Element pooltype=new Element("type"); //设置连接池类型
pooltype.setText(dsb.getType());
newpool.addContent(pooltype);
Element poolname=new Element("name");//设置连接池名字
poolname.setText(dsb.getName());
newpool.addContent(poolname);
Element pooldriver=new Element("driver"); //设置连接池驱动
pooldriver.addContent(dsb.getDriver());
newpool.addContent(pooldriver);
Element poolurl=new Element("url");//设置连接池url
poolurl.setText(dsb.getUrl());
newpool.addContent(poolurl);
Element poolusername=new Element("username");//设置连接池用户名
poolusername.setText(dsb.getUsername());
newpool.addContent(poolusername);
Element poolpassword=new Element("password");//设置连接池密码
poolpassword.setText(dsb.getPassword());
newpool.addContent(poolpassword);
Element poolmaxconn=new Element("maxconn");//设置连接池最大连接
poolmaxconn.setText(String.valueOf(dsb.getMaxconn()));
newpool.addContent(poolmaxconn);
pools.add(newpool);//将child添加到root
Format format = Format.getPrettyFormat();
format.setIndent("");
format.setEncoding("utf-8");
XMLOutputter outp = new XMLOutputter(format);
fo = new FileOutputStream(rpath);
outp.output(doc, fo);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally
{
}
}
/**
*删除配置文件
*/
public void delConfigInfo(String path,String name)
{
String rpath=this.getClass().getResource("").getPath().substring(1)+path;
FileInputStream fi = null;
FileOutputStream fo=null;
try
{
fi=new FileInputStream(rpath);//读取路径文件
SAXBuilder sb=new SAXBuilder();
Document doc=sb.build(fi);
Element root=doc.getRootElement();
List pools=root.getChildren();
Element pool=null;
Iterator allPool=pools.iterator();
while(allPool.hasNext())
{
pool=(Element)allPool.next();
if(pool.getChild("name").getText().equals(name))
{
pools.remove(pool);
break;
}
}
Format format = Format.getPrettyFormat();
format.setIndent("");
format.setEncoding("utf-8");
XMLOutputter outp = new XMLOutputter(format);
fo = new FileOutputStream(rpath);
outp.output(doc, fo);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JDOMException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
finally
{
try {
fi.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
/**
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
// TODO Auto-generated method stub
ParseDSConfig pd=new ParseDSConfig();
String path="ds.config.xml";
pd.readConfigInfo(path);
//pd.delConfigInfo(path, "tj012006");
DSConfigBean dsb=new DSConfigBean();
dsb.setType("oracle");
dsb.setName("yyy004");
dsb.setDriver("org.oracle.jdbc");
dsb.setUrl("jdbc:oracle://localhost");
dsb.setUsername("sa");
dsb.setPassword("");
dsb.setMaxconn(1000);
pd.addConfigInfo(path, dsb);
pd.delConfigInfo(path, "yyy001");
}
}
ds.config.xml 配置文件
--------------------------------------
<ds-config>
<pool>
<type>mysql</type>
<name>user</name>
<driver>com.mysql.jdbc.driver</driver>
<url>jdbc:mysql://localhost:3306/user</url>
<username>sa</username>
<password>123456</password>
<maxconn>100</maxconn>
</pool>
<pool>
<type>mysql</type>
<name>user2</name>
<driver>com.mysql.jdbc.driver</driver>
<url>jdbc:mysql://localhost:3306/user2</url>
<username>sa</username>
<password>1234</password>
<maxconn>10</maxconn>
</pool>
<pool>
<type>sql2000</type>
<name>books</name>
<driver>com.microsoft.sqlserver.driver</driver>
<url>jdbc:sqlserver://localhost:1433/books:databasename=books</url>
<username>sa</username>
<password></password>
<maxconn>100</maxconn>
</pool>
</ds-config>
3. 连接池的使用
1。Connection的获得和释放
DBConnectionManager connectionMan=DBConnectionManager .getInstance();//得到唯一实例
//得到连接
String name="mysql";//从上下文得到你要访问的数据库的名字
Connection con=connectionMan.getConnection(name);
//使用
。
// 使用完毕
connectionMan.freeConnection(name,con);//释放,但并未断开连接
2。数据库连接的动态增加和连接池的动态增加
1。调用xml操作增加类 2。重新实例华连接池管理池类
数据库连接池在编写应用服务是经常需要用到的模块,太过频繁的连接数据库对服务性能来讲是一个瓶颈,使用缓冲池技术可以来消除这个瓶颈。我们可以在互联网上找到很多关于数据库连接池的源程序,但是都发现这样一个共同的问题:这些连接池的实现方法都不同程度地增加了与使用者之间的耦合度。很多的连接池都要求用户通过其规定的方法获取数据库的连接,这一点我们可以理解,毕竟目前所有的应用服务器取数据库连接的方式都是这种方式实现的。但是另外一个共同的问题是,它们同时不允许使用者显式的调用Connection.close()方法,而需要用其规定的一个方法来关闭连接。这种做法有两个缺点:第一:改变了用户使用习惯,增加了用户的使用难度。
int executeSQL(String sql) throws SQLException
{
Connection conn = getConnection(); //通过某种方式获取数据库连接
PreparedStatement ps = null;
int res = 0;
try{
ps = conn.prepareStatement(sql);
res = ps.executeUpdate();
}finally{
try{
ps.close();
}catch(Exception e){}
try{
conn.close();//
}catch(Exception e){}
}
return res;
}
使用者在用完数据库连接后通常是直接调用连接的方法close来释放数据库资源,如果用我们前面提到的连接池的实现方法,那语句conn.close()将被某些特定的语句所替代。第二:使连接池无法对之中的所有连接进行独占控制。由于连接池不允许用户直接调用连接的close方法,一旦使用者在使用的过程中由于习惯问题直接关闭了数据库连接,那么连接池将无法正常维护所有连接的状态,考虑连接池和应用由不同开发人员实现时这种问题更容易出现。综合上面提到的两个问题,我们来讨论一下如何解决这两个要命的问题。首先我们先设身处地的考虑一下用户是想怎么样来使用这个数据库连接池的。用户可以通过特定的方法来获取数据库的连接,同时这个连接的类型应该是标准的java.sql.Connection。用户在获取到这个数据库连接后可以对这个连接进行任意的操作,包括关闭连接等。通过对用户使用的描述,怎样可以接管Connection.close方法就成了我们这篇文章的主题。为了接管数据库连接的close方法,我们应该有一种类似于钩子的机制。例如在Windows编程中我们可以利用Hook API来实现对某个Windows API的接管。在JAVA中同样也有这样一个机制。JAVA提供了一个Proxy类和一个InvocationHandler,这两个类都在java.lang.reflect包中。我们先来看看SUN公司提供的文档是怎么描述这两个类的。
public interface InvocationHandlerInvocationHandler is the interface implemented by the invocation handler of a proxy instance. Each proxy instance has an associated invocation handler.
When a method is invoked on a proxy instance,
the method invocation is encoded and dispatched to the invoke method of its invocation handler.
SUN的API文档中关于Proxy的描述很多,这里就不罗列出来。通过文档对接口InvocationHandler的描述我们可以看到当调用一个Proxy实例的方法时会触发Invocationhanlder的invoke方法。从JAVA的文档中我们也同时了解到这种动态代理机制只能接管接口的方法,而对一般的类无效,考虑到java.sql.Connection本身也是一个接口由此就找到了解决如何接管close方法的出路。首先,我们先定义一个数据库连接池参数的类,定义了数据库的JDBC驱动程序类名,连接的URL以及用户名口令等等一些信息,该类是用于初始化连接池的参数,具体定义如下:
public class ConnectionParam implements Serializable
{
private String driver; //数据库驱动程序
private String url; //数据连接的URL
private String user; //数据库用户名
private String password; //数据库密码
private int minConnection = 0; //初始化连接数
private int maxConnection = 50; //最大连接数
private long timeoutValue = 600000;//连接的最大空闲时间
private long waitTime = 30000; //取连接的时候如果没有可用连接最大的等待时间
其次是连接池的工厂类ConnectionFactory,通过该类来将一个连接池对象与一个名称对应起来,使用者通过该名称就可以获取指定的连接池对象,具体代码如下:
/**
* 连接池类厂,该类常用来保存多个数据源名称合数据库连接池对应的哈希
* @author liusoft
*/
public class ConnectionFactory
{
//该哈希表用来保存数据源名和连接池对象的关系表
static Hashtable connectionPools = null;
static{
connectionPools = new Hashtable(2,0.75F);
}
/**
* 从连接池工厂中获取指定名称对应的连接池对象
* @param dataSource 连接池对象对应的名称
* @return DataSource 返回名称对应的连接池对象
* @throws NameNotFoundException 无法找到指定的连接池
*/
public static DataSource lookup(String dataSource)
throws NameNotFoundException
{
Object ds = null;
ds = connectionPools.get(dataSource);
if(ds == null || !(ds instanceof DataSource))
throw new NameNotFoundException(dataSource);
return (DataSource)ds;
}
public static DataSource bind(String name, ConnectionParam param)
throws NameAlreadyBoundException,ClassNotFoundException,
IllegalAccessException,InstantiationException,SQLException
{
DataSourceImpl source = null;
try{
lookup(name);
throw new NameAlreadyBoundException(name);
}catch(NameNotFoundException e){
source = new DataSourceImpl(param);
source.initConnection();
connectionPools.put(name, source);
}
return source;
}
public static DataSource rebind(String name, ConnectionParam param)
throws NameAlreadyBoundException,ClassNotFoundException,
IllegalAccessException,InstantiationException,SQLException
{
try{
unbind(name);
}catch(Exception e){}
return bind(name, param);
}
public static void unbind(String name) throws NameNotFoundException
{
DataSource dataSource = lookup(name);
if(dataSource instanceof DataSourceImpl){
DataSourceImpl dsi = (DataSourceImpl)dataSource;
try{
dsi.stop();
dsi.close();
}catch(Exception e){
}finally{
dsi = null;
}
}
connectionPools.remove(name);
}
}
ConnectionFactory主要提供了用户将将连接池绑定到一个具体的名称上以及取消绑定的操作。使用者只需要关心这两个
String name = "pool;
String driver = " sun.jdbc.odbc.JdbcOdbcDriver ";
String url = "jdbc:odbc:datasource";
ConnectionParam param = new ConnectionParam(driver,url,null,null);
param.setMinConnection(1);
param.setMaxConnection(5);
param.setTimeoutValue(20000);
ConnectionFactory.bind(name, param);
System.out.println("bind datasource ok.");
//以上代码是用来登记一个连接池对象,该操作可以在程序初始化只做一次即可
//以下开始就是使用者真正需要写的代码
DataSource ds = ConnectionFactory.lookup(name);
try{
for(int i=0;i<10;i++){
Connection conn = ds.getConnection();
try{
testSQL(conn, sql);
}finally{
try{
conn.close();
}catch(Exception e){}
}
}
}catch(Exception e){
e.printStackTrace();
}finally{
ConnectionFactory.unbind(name);
System.out.println("unbind datasource ok.");
System.exit(0);
}从使用者的示例代码就可以看出,我们已经解决了常规连接池产生的两个问题。但是我们最最关心的是如何解决接管close方法的办法。接管工作主要在ConnectionFactory中的两句代码:
source = new DataSourceImpl(param);
source.initConnection();DataSourceImpl是一个实现了接口javax.sql.DataSource的类,该类维护着一个连接池的对象。由于该类是一个受保护的类,因此它暴露给使用者的方法只有接口DataSource中定义的方法,其他的所有方法对使用者来说都是不可视的。我们先来关心用户可访问的一个方法getConnection
public Connection getConnection(String user, String password) throws SQLException
{
//首先从连接池中找出空闲的对象
Connection conn = getFreeConnection(0);
if(conn == null){
//判断是否超过最大连接数,如果超过最大连接数
//则等待一定时间查看是否有空闲连接,否则抛出异常告诉用户无可用连接
if(getConnectionCount() >= connParam.getMaxConnection())
conn = getFreeConnection(connParam.getWaitTime());
else{//没有超过连接数,重新获取一个数据库的连接
connParam.setUser(user);
connParam.setPassword(password);
Connection conn2 = DriverManager.getConnection(connParam.getUrl(),
user, password);
//代理将要返回的连接对象
_Connection _conn = new _Connection(conn2,true);
synchronized(conns){
conns.add(_conn);
}
conn = _conn.getConnection();
}
}
return conn;
} protected synchronized Connection getFreeConnection(long nTimeout)
throws SQLException
{
Connection conn = null;
Iterator iter = conns.iterator();
while(iter.hasNext()){
_Connection _conn = (_Connection)iter.next();
if(!_conn.isInUse()){
conn = _conn.getConnection();
_conn.setInUse(true);
break;
}
}
if(conn == null && nTimeout > 0){
//等待nTimeout毫秒以便看是否有空闲连接
try{
Thread.sleep(nTimeout);
}catch(Exception e){}
conn = getFreeConnection(0);
if(conn == null)
throw new SQLException("没有可用的数据库连接");
}
return conn;
DataSourceImpl类中实现getConnection方法的跟正常的数据库连接池的逻辑是一致的,首先判断是否有空闲的连接,如果没有的话判断连接数是否已经超过最大连接数等等的一些逻辑。但是有一点不同的是通过DriverManager得到的数据库连接并不是及时返回的,而是通过一个叫_Connection的类中介一下,然后调用_Connection.getConnection返回的。如果我们没有通过一个中介也就是JAVA中的Proxy来接管要返回的接口对象,那么我们就没有办法截住Connection.close方法。
终于到了核心所在,我们先来看看_Connection是如何实现的,然后再介绍是客户端调用Connection.close方法时走的是怎样一个流程,为什么并没有真正的关闭连接。
class _Connection implements InvocationHandler
{
private final static String CLOSE_METHOD_NAME = "close";
private Connection conn = null;
//数据库的忙状态
private boolean inUse = false;
//用户最后一次访问该连接方法的时间
private long lastAccessTime = System.currentTimeMillis();
_Connection(Connection conn, boolean inUse){
this.conn = conn;
this.inUse = inUse;
} public Connection getConnection() {
//返回数据库连接conn的接管类,以便截住close方法
Connection conn2 = (Connection)Proxy.newProxyInstance(
conn.getClass().getClassLoader(),
conn.getClass().getInterfaces(),this);
return conn2;
} void close() throws SQLException{
//由于类属性conn是没有被接管的连接,因此一旦调用close方法后就直接关闭连接
conn.close();
} public boolean isInUse() {
return inUse;
} public Object invoke(Object proxy, Method m, Object[] args)
throws Throwable
{
Object obj = null;
//判断是否调用了close的方法,如果调用close方法则把连接置为无用状态
if(CLOSE_METHOD_NAME.equals(m.getName()))
setInUse(false);
else
obj = m.invoke(conn, args);
//设置最后一次访问时间,以便及时清除超时的连接
lastAccessTime = System.currentTimeMillis();
return obj;
} public long getLastAccessTime() {
return lastAccessTime;
} public void setInUse(boolean inUse) {
this.inUse = inUse;
}
}一旦使用者调用所得到连接的close方法,由于用户的连接对象是经过接管后的对象,因此JAVA虚拟机会首先调用_Connection.invoke方法,在该方法中首先判断是否为close方法,如果不是则将代码转给真正的没有被接管的连接对象conn。否则的话只是简单的将该连接的状态设置为可用。到此您可能就明白了整个接管的过程,但是同时也有一个疑问:这样的话是不是这些已建立的连接就始终没有办法真正关闭?答案是可以的。我们来看看ConnectionFactory.unbind方法,该方法首先找到名字对应的连接池对象,然后关闭该连接池中的所有连接并删除掉连接池。在DataSourceImpl类中定义了一个close方法用来关闭所有的连接,详细代码如下: public int close() throws SQLException
{
int cc = 0;
SQLException excp = null;
Iterator iter = conns.iterator();
while(iter.hasNext()){
try{
((_Connection)iter.next()).close();
cc ++;
}catch(Exception e){
if(e instanceof SQLException)
excp = (SQLException)e;
}
}
if(excp != null)
throw excp;
return cc;
}
关键字: Java, JDBC, Connection Pool, Database, 数据库连接池, sourcecode
虽然 J2EE 程序员一般都有现成的应用服务器所带的JDBC 数据库连接池,不过对于开发一般的 Java Application 、 Applet 或者 JSP、velocity 时,我们可用的JDBC 数据库连接池并不多,并且一般性能都不好。 Java 程序员都很羡慕 Windows ADO ,只需要 new Connection 就可以直接从数据库连接池中返回 Connection。并且 ADO Connection 是线程安全的,多个线程可以共用一个 Connection, 所以 ASP 程序一般都把 getConnection 放在 Global.asa 文件中,在 IIS 启动时建立数据库连接。ADO 的 Connection 和 Result 都有很好的缓冲,并且很容易使用。
其实我们可以自己写一个JDBC 数据库连接池。写 JDBC connection pool 的注意事项有:
1. 有一个简单的函数从连接池中得到一个 Connection。
2. close 函数必须将 connection 放回 数据库连接池。
3. 当数据库连接池中没有空闲的 connection, 数据库连接池必须能够自动增加 connection 个数。
4. 当数据库连接池中的 connection 个数在某一个特别的时间变得很大,但是以后很长时间只用其中一小部分,应该可以自动将多余的 connection 关闭掉。
5. 如果可能,应该提供debug 信息报告没有关闭的 new Connection 。
如果要 new Connection 就可以直接从数据库连接池中返回 Connection, 可以这样写( Mediator pattern ) (以下代码中使用了中文全角空格):
public class EasyConnection implements java.sql.Connection{
private Connection m_delegate = null;
public EasyConnection(){
m_delegate = getConnectionFromPool();
}
public void close(){
putConnectionBackToPool(m_delegate);
}
public PreparedStatement prepareStatement(String sql) throws SQLException{
m_delegate.prepareStatement(sql);
}
//...... other method
}
看来并不难。不过不建议这种写法,因为应该尽量避免使用 Java Interface, 关于 Java Interface 的缺点我另外再写文章讨论。大家关注的是 Connection Pool 的实现方法。下面给出一种实现方法。
import java.sql.*;
import java.lang.reflect.*;
import java.util.*;
import java.io.*;
public class SimpleConnetionPool {
private static LinkedList m_notUsedConnection = new LinkedList();
private static HashSet m_usedUsedConnection = new HashSet();
private static String m_url = "";
private static String m_user = "";
private static String m_password = "";
static final boolean DEBUG = true;
static private long m_lastClearClosedConnection = System.currentTimeMillis();
public static long CHECK_CLOSED_CONNECTION_TIME = 4 * 60 * 60 * 1000; //4 hours
static {
initDriver();
}
private SimpleConnetionPool() {
}
private static void initDriver() {
Driver driver = null;
//load mysql driver
try {
driver = (Driver) Class.forName("com.mysql.jdbc.Driver").newInstance();
installDriver(driver);
} catch (Exception e) {
}
//load postgresql driver
try {
driver = (Driver) Class.forName("org.postgresql.Driver").newInstance();
installDriver(driver);
} catch (Exception e) {
}
}
public static void installDriver(Driver driver) {
try {
DriverManager.registerDriver(driver);
} catch (Exception e) {
e.printStackTrace();
}
} public static synchronized Connection getConnection() {
clearClosedConnection();
while (m_notUsedConnection.size() > 0) {
try {
ConnectionWrapper wrapper = (ConnectionWrapper) m_notUsedConnection.removeFirst();
if (wrapper.connection.isClosed()) {
continue;
}
m_usedUsedConnection.add(wrapper);
if (DEBUG) {
wrapper.debugInfo = new Throwable("Connection initial statement");
}
return wrapper.connection;
} catch (Exception e) {
}
}
int newCount = getIncreasingConnectionCount();
LinkedList list = new LinkedList();
ConnectionWrapper wrapper = null;
for (int i = 0; i < newCount; i++) {
wrapper = getNewConnection();
if (wrapper != null) {
list.add(wrapper);
}
}
if (list.size() == 0) {
return null;
}
wrapper = (ConnectionWrapper) list.removeFirst();
m_usedUsedConnection.add(wrapper);
m_notUsedConnection.addAll(list);
list.clear();
return wrapper.connection;
}
private static ConnectionWrapper getNewConnection() {
try {
Connection con = DriverManager.getConnection(m_url, m_user, m_password);
ConnectionWrapper wrapper = new ConnectionWrapper(con);
return wrapper;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
static synchronized void pushConnectionBackToPool(ConnectionWrapper con) {
boolean exist = m_usedUsedConnection.remove(con);
if (exist) {
m_notUsedConnection.addLast(con);
}
}
public static int close() {
int count = 0;
Iterator iterator = m_notUsedConnection.iterator();
while (iterator.hasNext()) {
try {
( (ConnectionWrapper) iterator.next()).close();
count++;
} catch (Exception e) {
}
}
m_notUsedConnection.clear();
iterator = m_usedUsedConnection.iterator();
while (iterator.hasNext()) {
try {
ConnectionWrapper wrapper = (ConnectionWrapper) iterator.next();
wrapper.close();
if (DEBUG) {
wrapper.debugInfo.printStackTrace();
}
count++;
} catch (Exception e) {
}
}
m_usedUsedConnection.clear();
return count;
}
long time = System.currentTimeMillis();
//sometimes user change system time,just return
if (time < m_lastClearClosedConnection) {
time = m_lastClearClosedConnection;
return;
}
//no need check very often
if (time - m_lastClearClosedConnection < CHECK_CLOSED_CONNECTION_TIME) {
return;
}
m_lastClearClosedConnection = time;
//begin check
Iterator iterator = m_notUsedConnection.iterator();
while (iterator.hasNext()) {
ConnectionWrapper wrapper = (ConnectionWrapper) iterator.next();
try {
if (wrapper.connection.isClosed()) {
iterator.remove();
}
} catch (Exception e) {
iterator.remove();
if (DEBUG) {
System.out.println("connection is closed, this connection initial StackTrace");
wrapper.debugInfo.printStackTrace();
}
}
}
//make connection pool size smaller if too big
int decrease = getDecreasingConnectionCount();
if (m_notUsedConnection.size() < decrease) {
return;
}
while (decrease-- > 0) {
ConnectionWrapper wrapper = (ConnectionWrapper) m_notUsedConnection.removeFirst();
try {
wrapper.connection.close();
} catch (Exception e) {
}
}
}
/**
* get increasing connection count, not just add 1 connection
* @return count
*/
public static int getIncreasingConnectionCount() {
int count = 1;
int current = getConnectionCount();
count = current / 4;
if (count < 1) {
count = 1;
}
return count;
}
/**
* get decreasing connection count, not just remove 1 connection
* @return count
*/
public static int getDecreasingConnectionCount() {
int count = 0;
int current = getConnectionCount();
if (current < 10) {
return 0;
}
return current / 3;
}
public synchronized static void printDebugMsg() {
printDebugMsg(System.out);
}
public synchronized static void printDebugMsg(PrintStream out) {
if (DEBUG == false) {
return;
}
StringBuffer msg = new StringBuffer();
msg.append("debug message in " + SimpleConnetionPool.class.getName());
msg.append("\r\n");
msg.append("total count is connection pool: " + getConnectionCount());
msg.append("\r\n");
msg.append("not used connection count: " + getNotUsedConnectionCount());
msg.append("\r\n");
msg.append("used connection, count: " + getUsedConnectionCount());
out.println(msg);
Iterator iterator = m_usedUsedConnection.iterator();
while (iterator.hasNext()) {
ConnectionWrapper wrapper = (ConnectionWrapper) iterator.next();
wrapper.debugInfo.printStackTrace(out);
}
out.println();
}
public static synchronized int getNotUsedConnectionCount() {
return m_notUsedConnection.size();
}
public static synchronized int getUsedConnectionCount() {
return m_usedUsedConnection.size();
}
public static synchronized int getConnectionCount() {
return m_notUsedConnection.size() + m_usedUsedConnection.size();
}
public static String getUrl() {
return m_url;
}
public static void setUrl(String url) {
if (url == null) {
return;
}
m_url = url.trim();
}
public static String getUser() {
return m_user;
}
public static void setUser(String user) {
if (user == null) {
return;
}
m_user = user.trim();
}
public static String getPassword() {
return m_password;
}
public static void setPassword(String password) {
if (password == null) {
return;
}
m_password = password.trim();
}
}
class ConnectionWrapper implements InvocationHandler {
private final static String CLOSE_METHOD_NAME = "close";
public Connection connection = null;
private Connection m_originConnection = null;
public long lastAccessTime = System.currentTimeMillis();
Throwable debugInfo = new Throwable("Connection initial statement");
ConnectionWrapper(Connection con) {
Class[] interfaces = {java.sql.Connection.class};
this.connection = (Connection) Proxy.newProxyInstance(
con.getClass().getClassLoader(),
interfaces, this);
m_originConnection = con;
}
void close() throws SQLException {
m_originConnection.close();
}
public Object invoke(Object proxy, Method m, Object[] args) throws Throwable {
Object obj = null;
if (CLOSE_METHOD_NAME.equals(m.getName())) {
SimpleConnetionPool.pushConnectionBackToPool(this);
}
else {
obj = m.invoke(m_originConnection, args);
}
lastAccessTime = System.currentTimeMillis();
return obj;
}
}
使用方法
public class TestConnectionPool{
public static void main(String[] args) {
SimpleConnetionPool.setUrl(DBTools.getDatabaseUrl());
SimpleConnetionPool.setUser(DBTools.getDatabaseUserName());
SimpleConnetionPool.setPassword(DBTools.getDatabasePassword());
Connection con = SimpleConnetionPool.getConnection();
Connection con1 = SimpleConnetionPool.getConnection();
Connection con2 = SimpleConnetionPool.getConnection();
//do something with con ...
try {
con.close();
} catch (Exception e) {}
try {
con1.close();
} catch (Exception e) {}
try {
con2.close();
} catch (Exception e) {}
con = SimpleConnetionPool.getConnection();
con1 = SimpleConnetionPool.getConnection();
try {
con1.close();
} catch (Exception e) {}
con2 = SimpleConnetionPool.getConnection();
SimpleConnetionPool.printDebugMsg();
}
}
运行测试程序后打印连接池中 Connection 状态, 以及正在使用的没有关闭 Connection 信息。