隨筆-348  評論-598  文章-0  trackbacks-0

          使用JAVA中的動態(tài)代理實現(xiàn)數(shù)據(jù)庫連接池

          developerWorks
          文檔選項
          將此頁作為電子郵件發(fā)送

          將此頁作為電子郵件發(fā)送

          未顯示需要 JavaScript 的文檔選項


          拓展 Tomcat 應用

          下載 IBM 開源 J2EE 應用服務器 WAS CE 新版本 V1.1


          級別: 初級

          劉冬 (winter.lau@163.com), 珠海市創(chuàng)我科技發(fā)展有限公司軟件工程師

          2002 年 12 月 05 日

          作者通過使用JAVA中的動態(tài)代理實現(xiàn)數(shù)據(jù)庫連接池,使使用者可以以普通的jdbc連接的使用習慣來使用連接池。

          數(shù)據(jù)庫連接池在編寫應用服務是經(jīng)常需要用到的模塊,太過頻繁的連接數(shù)據(jù)庫對服務性能來講是一個瓶頸,使用緩沖池技術(shù)可以來消除這個瓶頸。我們可以在互聯(lián)網(wǎng)上找到很多關(guān)于數(shù)據(jù)庫連接池的源程序,但是都發(fā)現(xiàn)這樣一個共同的問題:這些連接池的實現(xiàn)方法都不同程度地增加了與使用者之間的耦合度。很多的連接池都要求用戶通過其規(guī)定的方法獲取數(shù)據(jù)庫的連接,這一點我們可以理解,畢竟目前所有的應用服務器取數(shù)據(jù)庫連接的方式都是這種方式實現(xiàn)的。但是另外一個共同的問題是,它們同時不允許使用者顯式的調(diào)用Connection.close()方法,而需要用其規(guī)定的一個方法來關(guān)閉連接。這種做法有兩個缺點:

          第一:改變了用戶使用習慣,增加了用戶的使用難度。

          首先我們來看看一個正常的數(shù)據(jù)庫操作過程:

          int executeSQL(String sql) throws SQLException
                                  {
                                  Connection conn = getConnection();	//通過某種方式獲取數(shù)據(jù)庫連接
                                  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;
                                  }
                                  

          使用者在用完數(shù)據(jù)庫連接后通常是直接調(diào)用連接的方法close來釋放數(shù)據(jù)庫資源,如果用我們前面提到的連接池的實現(xiàn)方法,那語句conn.close()將被某些特定的語句所替代。

          第二:使連接池無法對之中的所有連接進行獨占控制。由于連接池不允許用戶直接調(diào)用連接的close方法,一旦使用者在使用的過程中由于習慣問題直接關(guān)閉了數(shù)據(jù)庫連接,那么連接池將無法正常維護所有連接的狀態(tài),考慮連接池和應用由不同開發(fā)人員實現(xiàn)時這種問題更容易出現(xiàn)。

          綜合上面提到的兩個問題,我們來討論一下如何解決這兩個要命的問題。

          首先我們先設身處地的考慮一下用戶是想怎么樣來使用這個數(shù)據(jù)庫連接池的。用戶可以通過特定的方法來獲取數(shù)據(jù)庫的連接,同時這個連接的類型應該是標準的java.sql.Connection。用戶在獲取到這個數(shù)據(jù)庫連接后可以對這個連接進行任意的操作,包括關(guān)閉連接等。

          通過對用戶使用的描述,怎樣可以接管Connection.close方法就成了我們這篇文章的主題。

          為了接管數(shù)據(jù)庫連接的close方法,我們應該有一種類似于鉤子的機制。例如在Windows編程中我們可以利用Hook API來實現(xiàn)對某個Windows API的接管。在JAVA中同樣也有這樣一個機制。JAVA提供了一個Proxy類和一個InvocationHandler,這兩個類都在java.lang.reflect包中。我們先來看看SUN公司提供的文檔是怎么描述這兩個類的。

          public interface InvocationHandler
                                  InvocationHandler 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文檔中關(guān)于Proxy的描述很多,這里就不羅列出來。通過文檔對接口InvocationHandler的描述我們可以看到當調(diào)用一個Proxy實例的方法時會觸發(fā)Invocationhanlder的invoke方法。從JAVA的文檔中我們也同時了解到這種動態(tài)代理機制只能接管接口的方法,而對一般的類無效,考慮到java.sql.Connection本身也是一個接口由此就找到了解決如何接管close方法的出路。

          首先,我們先定義一個數(shù)據(jù)庫連接池參數(shù)的類,定義了數(shù)據(jù)庫的JDBC驅(qū)動程序類名,連接的URL以及用戶名口令等等一些信息,該類是用于初始化連接池的參數(shù),具體定義如下:

          public class ConnectionParam implements Serializable
                                  {
                                  private String driver;				//數(shù)據(jù)庫驅(qū)動程序
                                  private String url;					//數(shù)據(jù)連接的URL
                                  private String user;					//數(shù)據(jù)庫用戶名
                                  private String password;				//數(shù)據(jù)庫密碼
                                  private int minConnection = 0;		//初始化連接數(shù)
                                  private int maxConnection = 50;		//最大連接數(shù)
                                  private long timeoutValue = 600000;//連接的最大空閑時間
                                  private long waitTime = 30000;		//取連接的時候如果沒有可用連接最大的等待時間
                                  

          其次是連接池的工廠類ConnectionFactory,通過該類來將一個連接池對象與一個名稱對應起來,使用者通過該名稱就可以獲取指定的連接池對象,具體代碼如下:

          /**
                                  * 連接池類廠,該類常用來保存多個數(shù)據(jù)源名稱合數(shù)據(jù)庫連接池對應的哈希
                                  * @author liusoft
                                  */
                                  public class ConnectionFactory
                                  {
                                  //該哈希表用來保存數(shù)據(jù)源名和連接池對象的關(guān)系表
                                  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;
                                  }
                                  /**
                                  * 將指定的名字和數(shù)據(jù)庫連接配置綁定在一起并初始化數(shù)據(jù)庫連接池
                                  * @param name		對應連接池的名稱
                                  * @param param	連接池的配置參數(shù),具體請見類ConnectionParam
                                  * @return DataSource	如果綁定成功后返回連接池對象
                                  * @throws NameAlreadyBoundException	一定名字name已經(jīng)綁定則拋出該異常
                                  * @throws ClassNotFoundException		無法找到連接池的配置中的驅(qū)動程序類
                                  * @throws IllegalAccessException		連接池配置中的驅(qū)動程序類有誤
                                  * @throws InstantiationException		無法實例化驅(qū)動程序類
                                  * @throws SQLException				無法正常連接指定的數(shù)據(jù)庫
                                  */
                                  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;
                                  }
                                  /**
                                  * 重新綁定數(shù)據(jù)庫連接池
                                  * @param name		對應連接池的名稱
                                  * @param param	連接池的配置參數(shù),具體請見類ConnectionParam
                                  * @return DataSource	如果綁定成功后返回連接池對象
                                  * @throws NameAlreadyBoundException	一定名字name已經(jīng)綁定則拋出該異常
                                  * @throws ClassNotFoundException		無法找到連接池的配置中的驅(qū)動程序類
                                  * @throws IllegalAccessException		連接池配置中的驅(qū)動程序類有誤
                                  * @throws InstantiationException		無法實例化驅(qū)動程序類
                                  * @throws SQLException				無法正常連接指定的數(shù)據(jù)庫
                                  */
                                  public static DataSource rebind(String name, ConnectionParam param)
                                  throws NameAlreadyBoundException,ClassNotFoundException,
                                  IllegalAccessException,InstantiationException,SQLException
                                  {
                                  try{
                                  unbind(name);
                                  }catch(Exception e){}
                                  return bind(name, param);
                                  }
                                  /**
                                  * 刪除一個數(shù)據(jù)庫連接池對象
                                  * @param name
                                  * @throws NameNotFoundException
                                  */
                                  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主要提供了用戶將將連接池綁定到一個具體的名稱上以及取消綁定的操作。使用者只需要關(guān)心這兩個類即可使用數(shù)據(jù)庫連接池的功能。下面我們給出一段如何使用連接池的代碼:

          	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);
                                  }
                                  

          從使用者的示例代碼就可以看出,我們已經(jīng)解決了常規(guī)連接池產(chǎn)生的兩個問題。但是我們最最關(guān)心的是如何解決接管close方法的辦法。接管工作主要在ConnectionFactory中的兩句代碼:

          source = new DataSourceImpl(param);
                                  source.initConnection();
                                  

          DataSourceImpl是一個實現(xiàn)了接口javax.sql.DataSource的類,該類維護著一個連接池的對象。由于該類是一個受保護的類,因此它暴露給使用者的方法只有接口DataSource中定義的方法,其他的所有方法對使用者來說都是不可視的。我們先來關(guān)心用戶可訪問的一個方法getConnection

          /**
                                  * @see javax.sql.DataSource#getConnection(String,String)
                                  */
                                  public Connection getConnection(String user, String password) throws SQLException
                                  {
                                  //首先從連接池中找出空閑的對象
                                  Connection conn = getFreeConnection(0);
                                  if(conn == null){
                                  //判斷是否超過最大連接數(shù),如果超過最大連接數(shù)
                                  //則等待一定時間查看是否有空閑連接,否則拋出異常告訴用戶無可用連接
                                  if(getConnectionCount() >= connParam.getMaxConnection())
                                  conn = getFreeConnection(connParam.getWaitTime());
                                  else{//沒有超過連接數(shù),重新獲取一個數(shù)據(jù)庫的連接
                                  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;
                                  }
                                  /**
                                  * 從連接池中取一個空閑的連接
                                  * @param nTimeout	如果該參數(shù)值為0則沒有連接時只是返回一個null
                                  * 否則的話等待nTimeout毫秒看是否還有空閑連接,如果沒有拋出異常
                                  * @return Connection
                                  * @throws SQLException
                                  */
                                  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("沒有可用的數(shù)據(jù)庫連接");
                                  }
                                  return conn;
                                  }
                                  

          DataSourceImpl類中實現(xiàn)getConnection方法的跟正常的數(shù)據(jù)庫連接池的邏輯是一致的,首先判斷是否有空閑的連接,如果沒有的話判斷連接數(shù)是否已經(jīng)超過最大連接數(shù)等等的一些邏輯。但是有一點不同的是通過DriverManager得到的數(shù)據(jù)庫連接并不是及時返回的,而是通過一個叫_Connection的類中介一下,然后調(diào)用_Connection.getConnection返回的。如果我們沒有通過一個中介也就是JAVA中的Proxy來接管要返回的接口對象,那么我們就沒有辦法截住Connection.close方法。

          終于到了核心所在,我們先來看看_Connection是如何實現(xiàn)的,然后再介紹是客戶端調(diào)用Connection.close方法時走的是怎樣一個流程,為什么并沒有真正的關(guān)閉連接。

          /**
                                  * 數(shù)據(jù)連接的自封裝,屏蔽了close方法
                                  * @author Liudong
                                  */
                                  class _Connection implements InvocationHandler
                                  {
                                  private final static String CLOSE_METHOD_NAME = "close";
                                  private Connection conn = null;
                                  //數(shù)據(jù)庫的忙狀態(tài)
                                  private boolean inUse = false;
                                  //用戶最后一次訪問該連接方法的時間
                                  private long lastAccessTime = System.currentTimeMillis();
                                  _Connection(Connection conn, boolean inUse){
                                  this.conn = conn;
                                  this.inUse = inUse;
                                  }
                                  /**
                                  * Returns the conn.
                                  * @return Connection
                                  */
                                  public Connection getConnection() {
                                  //返回數(shù)據(jù)庫連接conn的接管類,以便截住close方法
                                  Connection conn2 = (Connection)Proxy.newProxyInstance(
                                  conn.getClass().getClassLoader(),
                                  conn.getClass().getInterfaces(),this);
                                  return conn2;
                                  }
                                  /**
                                  * 該方法真正的關(guān)閉了數(shù)據(jù)庫的連接
                                  * @throws SQLException
                                  */
                                  void close() throws SQLException{
                                  //由于類屬性conn是沒有被接管的連接,因此一旦調(diào)用close方法后就直接關(guān)閉連接
                                  conn.close();
                                  }
                                  /**
                                  * Returns the inUse.
                                  * @return boolean
                                  */
                                  public boolean isInUse() {
                                  return inUse;
                                  }
                                  /**
                                  * @see java.lang.reflect.InvocationHandler#invoke(java.lang.Object, java.lang.reflect.Method, java.lang.Object)
                                  */
                                  public Object invoke(Object proxy, Method m, Object[] args)
                                  throws Throwable
                                  {
                                  Object obj = null;
                                  //判斷是否調(diào)用了close的方法,如果調(diào)用close方法則把連接置為無用狀態(tài)
                                  if(CLOSE_METHOD_NAME.equals(m.getName()))
                                  setInUse(false);
                                  else
                                  obj = m.invoke(conn, args);
                                  //設置最后一次訪問時間,以便及時清除超時的連接
                                  lastAccessTime = System.currentTimeMillis();
                                  return obj;
                                  }
                                  /**
                                  * Returns the lastAccessTime.
                                  * @return long
                                  */
                                  public long getLastAccessTime() {
                                  return lastAccessTime;
                                  }
                                  /**
                                  * Sets the inUse.
                                  * @param inUse The inUse to set
                                  */
                                  public void setInUse(boolean inUse) {
                                  this.inUse = inUse;
                                  }
                                  }
                                  

          一旦使用者調(diào)用所得到連接的close方法,由于用戶的連接對象是經(jīng)過接管后的對象,因此JAVA虛擬機會首先調(diào)用_Connection.invoke方法,在該方法中首先判斷是否為close方法,如果不是則將代碼轉(zhuǎn)給真正的沒有被接管的連接對象conn。否則的話只是簡單的將該連接的狀態(tài)設置為可用。到此您可能就明白了整個接管的過程,但是同時也有一個疑問:這樣的話是不是這些已建立的連接就始終沒有辦法真正關(guān)閉?答案是可以的。我們來看看ConnectionFactory.unbind方法,該方法首先找到名字對應的連接池對象,然后關(guān)閉該連接池中的所有連接并刪除掉連接池。在DataSourceImpl類中定義了一個close方法用來關(guān)閉所有的連接,詳細代碼如下:

          	/**
                                  * 關(guān)閉該連接池中的所有數(shù)據(jù)庫連接
                                  * @return int 返回被關(guān)閉連接的個數(shù)
                                  * @throws SQLException
                                  */
                                  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;
                                  }
                                  

          該方法一一調(diào)用連接池中每個對象的close方法,這個close方法對應的是_Connection中對close的實現(xiàn),在_Connection定義中關(guān)閉數(shù)據(jù)庫連接的時候是直接調(diào)用沒有經(jīng)過接管的對象的關(guān)閉方法,因此該close方法真正的釋放了數(shù)據(jù)庫資源。

          以上文字只是描述了接口方法的接管,具體一個實用的連接池模塊還需要對空閑連接的監(jiān)控并及時釋放連接,詳細的代碼請參照附件。



          參考資料

          http://java.sun.com

          JAVA的官方網(wǎng)站



          關(guān)于作者

           

          劉冬,珠海市創(chuàng)我科技發(fā)展有限公司軟件工程師,主要從事J2EE方面的開發(fā)。電子郵件: winter.lau@163.com






          ---------------------------------------------------------
          專注移動開發(fā)

          Android, Windows Mobile, iPhone, J2ME, BlackBerry, Symbian
          posted on 2007-05-09 20:42 TiGERTiAN 閱讀(315) 評論(0)  編輯  收藏 所屬分類: Java
          主站蜘蛛池模板: 稷山县| 界首市| 农安县| 濮阳县| 内江市| 丹寨县| 中牟县| 商洛市| 榕江县| 景德镇市| 鱼台县| 奉新县| 临西县| 海原县| 信阳市| 精河县| 襄城县| 平陆县| 永安市| 佛坪县| 静安区| 西昌市| 盐山县| 广昌县| 犍为县| 剑河县| 五峰| 东城区| 霍林郭勒市| 都江堰市| 阿鲁科尔沁旗| 冕宁县| 乐昌市| 邵阳县| 庆云县| 祁门县| 海门市| 吴堡县| 年辖:市辖区| 革吉县| 萍乡市|