1. 程式人生 > >JDBC/InvocationHandler動態代理實現資料庫連線池、資料來源

JDBC/InvocationHandler動態代理實現資料庫連線池、資料來源

Java的JDBC程式設計中,資料庫的連線和關閉是十分耗費資源的。當對資料庫的訪問不是很頻繁時,可以在每次訪問資料庫時建立一個連線,用完之後關閉。但是,對於一個複雜的資料庫應用,頻繁的建立、關閉連線,會極大的減低系統性能,造成瓶頸。所以可以使用資料庫連線池來達到連線資源的共享,使得對於資料庫的連線可以使高效、安全的複用。

MyDataSource 實現資料庫連線池

通過自定義資料庫了連線MyConnection(包裹了真正的Connection),使用一個LinkedList存放MyConnection,當一個數據庫連線使用完成後,重新新增到LinkedList中,實現連線的複用。資料庫連線池初始化時,構造多個數據庫連線,雖然此時比較耗時,但是能夠實現連線池的複用,提高效率。

package com.jdbc.datasource;

import java.sql.*;
import java.util.LinkedList;

/**
 * 資料庫連線池
 * 自定義實現資料來源
 *
 */
public class MyDataSource {

    private static final String URL = "jdbc:postgresql://localhost:5432/db";
    private static final String USER = "postgres";
    private static final String PASSWORD = "root"
; private static final int INI_COUNT = 5; // 初始連線數 private static final int MAX_COUNT = 10; // 最大連線數 public int curCount= 0; // 當前連線數 // add remove頻繁,LinkedList 效率由於 ArrayList LinkedList<Connection> connsPool = new LinkedList<Connection>(); /** * 初始構造多個數據庫連線 */
public MyDataSource() { try { for (int i = 0; i < INI_COUNT; i++) { this.connsPool.add(this.createConnection()); curCount++; } } catch (SQLException e) { e.printStackTrace(); } } /** * 建立連線 * @return * @throws SQLException */ public Connection createConnection() throws SQLException { Connection realConn = DriverManager.getConnection(URL, USER, PASSWORD); // MyConnection conn = new MyConnection(this, realConn); /*** Connection 的代理類,繫結真正的Connection,攔截 close()方法 ***/ MyConnectionHandler pHandler = new MyConnectionHandler(this); return pHandler.bind(realConn); } /** * 釋放 * @param conn 資料庫連線 */ public void free(Connection conn) { this.connsPool.addLast(conn); } /** * 獲取連線 * @return * @throws SQLException */ public Connection getConnection() throws SQLException { Connection conn = null; /*** 同步加鎖 ***/ synchronized (connsPool) { if (this.connsPool.size() > 0) { conn = this.connsPool.removeFirst(); return conn; } else if (curCount < MAX_COUNT) { // 連線池裡面沒有連線,且當前連線數沒有達到最大連線 this.curCount++; // 建立新連線 conn = this.createConnection(); return conn; } throw new SQLException(" 連線池裡已無可用連線 ... "); } } }

MyConnection 實現 Connection介面方式

將自定義類MyConnection實現Connection介面,重寫close()方法,關閉時重新放入連線池,其他的非close()方法則直接轉交給真正的Connection實現即可。該方式的缺點是,需要實現Connection介面的所有方法。

package com.jdbc.datasource;

import java.sql.*;
import java.util.*;
import java.util.concurrent.Executor;

/**
 * MyConnection 代理 Connection,實現 Connection介面
 * 相當於Connection的子類,可以和Connection一樣操作
 * 代理了所有對真正的Connection操作
 * 重要:close()方法重寫,關閉時重新放入連線池
 * 不足:需要重寫所有方法
 * 改進:改用Proxy代理模式實現,只對close()方法進行攔截,不修改其他方法
 *
 */
public class MyConnection implements Connection {

    private MyDataSource myDataSource = null; // 資料來源
    private Connection realConn = null; // 真正的connection

    private static final int MAX_USE_COUNT = 5; // 最大使用次數
    private int curUseCount = 0;    // 當前使用次數

    MyConnection(MyDataSource myDataSource, Connection realConn) {
        this.myDataSource = myDataSource;
        this.realConn = realConn;
    }


    /**
     * close()方法重寫
     * 未超過最大使用次數,關閉時重新放入連線池
     * @throws SQLException
     */
    @Override
    public void close() throws SQLException {
        curUseCount++;
        if (curUseCount < MAX_USE_COUNT) {
            this.myDataSource.connsPool.addLast(this);
        } else {
            this.realConn.close();
            this.myDataSource.curCount--;
        }
    }

    /**
     * 其他方法直接交給realConn實現
     * @throws SQLException
     */
    @Override
    public boolean isClosed() throws SQLException {
        return this.realConn.isClosed();
    }

    //其他方法略...
}

InvocationHandler動態代理實現方式

MyConnectionHandler類實現InvocationHandler介面,最主要包括兩個步驟:

  • Proxy.newProxyInstance()方法
    this.warpedConn = (Connection) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[] { Connection.class }, this); 使用包裹後的Connection代理真正的Connection
  • 重寫invoke()方法
    public Object invoke(Object proxy, Method method, Object[] args)攔截代理對應的方法。
package com.jdbc.datasource;

import java.lang.reflect.*;
import java.sql.Connection;

/**
 * Connection 代理類
 * 攔截 close()方法
 *
 */
public class MyConnectionHandler implements InvocationHandler {

    private Connection realConn = null; // 真正的連線
    private Connection warpedConn = null;   // 包裹的連線(代理連線)
    private MyDataSource myDataSource = null;   // 連線池

    private static final int MAX_USE_COUNT = 5; // 最大使用次數
    private int curUseCount = 0;    // 當前使用次數

    /**
     * 構造方法
     * @param myDataSource 連線池
     */
    public MyConnectionHandler(MyDataSource myDataSource) {
        this.myDataSource = myDataSource;
    }

    /**
     * 繫結真正的連線到包裹連線
     * @param realConn 真正的連線
     * @return
     */
    public Connection bind(Connection realConn) {
        /*** 真正的連線,用於資料庫的其他操作 ***/
        this.realConn = realConn;

        /*** 代理模式動態生成類 ,實現了Connection.Class 介面, 其方法作用在當前handler上 ***/
        /*** 用於攔截 realConn.close() 方法 ***/
        this.warpedConn = (Connection) Proxy.newProxyInstance(this.getClass()
                .getClassLoader(), new Class[] { Connection.class }, this);

        return this.warpedConn; // 返回包裹後的連線
    }

    /**
     * 重寫 invoke() 方法
     * 攔截close()方法,關閉連線是重新放回到資料庫連線池
     */
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        /*** 攔截 realConn.close() 方法 ***/
        if (method.getName().equals("close")) {
            curUseCount++;
            if (curUseCount < MAX_USE_COUNT) {
                this.myDataSource.connsPool.addLast(this.warpedConn);
            } else {
                this.realConn.close();
                this.myDataSource.curCount--;
            }
        }

        /*** 其他方法,直接執行到realConn上 ***/
        return method.invoke(this.realConn, args);
    }
}

Apache Commons DBCP 資料來源
資料來源一般不需要自己實現,apache DBCP資料來源就是一種很好的開源實現。

將資料來源的相關配置資訊以配置檔案的方式進行設定,讀取配置檔案,生成資料來源即可。DBCP資料來源會自動管理資料庫連線池,JDBC操作直接從資料來源中獲取Connection即可。

DBCP資料來源配置資訊:

#連線設定
driverClassName=org.postgresql.Driver
url=jdbc:postgresql://localhost:5432/db
username=postgres
password=root

#<!-- 初始化連線 -->
initialSize=10

#最大連線數量
maxActive=50

#<!-- 最大空閒連線 -->
maxIdle=20

#<!-- 最小空閒連線 -->
minIdle=5

#<!-- 超時等待時間以毫秒為單位 6000毫秒/1000等於60秒 -->
maxWait=60000


#JDBC驅動建立連線時附帶的連線屬性屬性的格式必須為這樣:[屬性名=property;] 
#注意:"user""password" 兩個屬性會被明確地傳遞,因此這裡不需要包含他們。
connectionProperties=useUnicode=true;characterEncoding=gbk

#指定由連線池所建立的連線的自動提交(auto-commit)狀態。
defaultAutoCommit=true

#driver default 指定由連線池所建立的連線的只讀(read-only)狀態。
#如果沒有設定該值,則“setReadOnly”方法將不被呼叫。(某些驅動並不支援只讀模式,如:Informix)
defaultReadOnly=

#driver default 指定由連線池所建立的連線的事務級別(TransactionIsolation)。
#可用值為下列之一:(詳情可見javadoc。)NONE,READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE
defaultTransactionIsolation=READ_UNCOMMITTED

JdbcUtil類使用DBCP資料來源:

package com.utils;

import java.io.*;
import java.sql.*;
import java.util.Properties;
import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSourceFactory;
//import com.jdbc.datasource.MyDataSource;

public class JdbcUtil {
/*  private static final String URL = "jdbc:postgresql://localhost:5432/db";
    private static final String USER = "postgres";
    private static final String PASSWORD = "root";*/

    // 使用自定義資料來源
    // private static MyDataSource myDataSource = null;

    // 使用Apache的框架,提供資料來源,實現了DataSource介面
    private static DataSource myDataSource = null;

    /**
     * 私有構造方法
     */
    private JdbcUtil() {

    }

    /**
     * 靜態程式碼註冊驅動
     */
    static {
        try {
            Class.forName("org.postgresql.Driver").newInstance();

            // 使用自定義資料來源
            // myDataSource = new MyDataSource();

            // 使用Apache的框架,提供資料來源,實現了DataSource介面
            Properties prop = new Properties();
            InputStream is = JdbcUtil.class.getClassLoader().
                    getResourceAsStream("dbcpconfig.properties");
            prop.load(is);
            myDataSource = BasicDataSourceFactory.createDataSource(prop);
        } catch (Exception e) {
            e.printStackTrace();
        } 
    }

    /**
     * 獲取datasource
     * @return
     */
    public static final DataSource GetDataSource() {
        return myDataSource;
    }


    /**
     * 獲取資料庫連線
     * @return
     */
    public static final Connection GetConnection() {
        Connection conn = null;
        try {
            // conn = DriverManager.getConnection(URL, USER, PASSWORD);
            conn = myDataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }

    /**
     * 關閉相關資源
     * @param rs 
     * @param st
     * @param conn
     */
    public static final void Free(ResultSet rs, Statement st, Connection conn) {
        try {
            if (rs != null)
                rs.close();
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (st != null)
                    st.close();
            } catch (SQLException e) {
                e.printStackTrace();
            } finally {
                if (conn != null)
                    try {
                        conn.close();
                        // myDataSource.free(conn);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
            }
        }
    }
}