首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java 動態代理實現數據庫連接池:攔截 close() 實現連接復用的零侵入式架構設計

     編輯:Java 程式設計入門

前言:連接池的兩大痛點

傳統連接池迫使用戶:

  1. 改變調用習慣:不能調用 `conn.close()`,必須調用池專有的 `releaseConnection()` 或 `freeConnection()`
  2. 失去狀態控制:用戶誤調 `close()` 導致連接真正關閉,池感知不到,出現「池以為連接空閒,實則已失效」的狀態不一致

本文展示如何利用 JDK 動態代理 (Proxy + InvocationHandler) 完美解決這兩個問題:用戶代碼零修改,透明復用連接。

一、核心架構設計

[用戶代碼] ──► DataSource.getConnection() 
    │                    │
    │                    ▼
    │            [DriverManager.getConnection()] ──► [真實 Connection]
    │                    │
    │                    ▼
    │            [_Connection 包裝器] ──實現 InvocationHandler
    │                    │
    │                    ▼
    │            [Proxy.newProxyInstance()] ──► [代理 Connection] ◄── 返回用戶
    │
    ▼
[用戶調用 proxy.close()] 
    │
    ▼
[InvocationHandler.invoke() 攔截]
    │
    ├── 非 close 方法 ──► 轉發真實 Connection
    │
    └── close 方法 ──► 標記連接空閒 (不關閉真實連接) ──► 返回

二、關鍵組件實現

1. 連接池參數配置類


public class ConnectionParam implements Serializable {
    private String driver;          // JDBC 驅動類名
    private String url;             // 數據庫 URL
    private String user;            // 用戶名
    private String password;        // 密碼
    private int minConnection = 0;  // 初始連接數
    private int maxConnection = 50; // 最大連接數
    private long timeoutValue = 600_000; // 最大空閒時間 (10分鐘)
    private long waitTime = 30_000;      // 獲取連接最大等待時間
    // getters/setters...
}

2. 連接池工廠:命名綁定與查找


public class ConnectionFactory {
    private static final Hashtable pools = new Hashtable<>();
    
    // 綁定名稱與連接池
    public static DataSource bind(String name, ConnectionParam param) 
            throws NamingException, SQLException {
        if (pools.containsKey(name)) 
            throw new NameAlreadyBoundException(name);
        
        DataSourceImpl pool = new DataSourceImpl(param);
        pool.initConnections(); // 初始化最小連接數
        pools.put(name, pool);
        return pool;
    }
    
    // 查找連接池
    public static DataSource lookup(String name) throws NamingException {
        DataSource ds = pools.get(name);
        if (ds == null) throw new NameNotFoundException(name);
        return ds;
    }
    
    // 銷毀連接池
    public static void unbind(String name) throws NamingException {
        DataSource ds = lookup(name);
        if (ds instanceof DataSourceImpl) {
            ((DataSourceImpl) ds).close(); // 真正關閉所有物理連接
        }
        pools.remove(name);
    }
}

3. 核心攔截器:_Connection 實現 InvocationHandler


class _Connection implements InvocationHandler {
    private static final String CLOSE_METHOD = "close";
    
    private final Connection realConn;      // 真實物理連接 (未被代理)
    private volatile boolean inUse = false; // 連接狀態
    private volatile long lastAccessTime = System.currentTimeMillis();
    
    _Connection(Connection conn, boolean inUse) {
        this.realConn = conn;
        this.inUse = inUse;
    }
    
    // 關鍵:生成代理 Connection 返回給用戶
    public Connection getProxyConnection() {
        return (Connection) Proxy.newProxyInstance(
            realConn.getClass().getClassLoader(),
            realConn.getClass().getInterfaces(), // Connection 是接口,滿足動態代理要求
            this
        );
    }
    
    // 真正關閉物理連接 (僅池銷毀時調用)
    void closeReal() throws SQLException {
        realConn.close();
    }
    
    // 攔截所有方法調用
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 更新最後訪問時間 (用於空閒超時回收)
        lastAccessTime = System.currentTimeMillis();
        
        String methodName = method.getName();
        if (CLOSE_METHOD.equals(methodName)) {
            // 攔截 close():標記空閒,不關閉真實連接!
            setInUse(false);
            return null; // close() 返回 void
        }
        
        // 其他方法透明轉發給真實連接
        try {
            return method.invoke(realConn, args);
        } catch (InvocationTargetException e) {
            throw e.getCause(); // 保留原始異常堆棧
        }
    }
    
    // 狀態管理
    public boolean isInUse() { return inUse; }
    public void setInUse(boolean inUse) { this.inUse = inUse; }
    public long getLastAccessTime() { return lastAccessTime; }
}

4. 連接池實現:DataSourceImpl


class DataSourceImpl implements DataSource {
    private final ConnectionParam param;
    private final List<_Connection> pool = Collections.synchronizedList(new ArrayList<>());
    private final Object poolLock = new Object();
    
    public DataSourceImpl(ConnectionParam param) { this.param = param; }
    
    // 初始化最小連接數
    public void initConnections() throws SQLException {
        for (int i = 0; i < param.getMinConnection(); i++) {
            createPhysicalConnection();
        }
    }
    
    @Override
    public Connection getConnection() throws SQLException {
        return getConnection(param.getUser(), param.getPassword());
    }
    
    @Override
    public Connection getConnection(String user, String password) throws SQLException {
        _Connection wrapper = getFreeWrapper(0); // 先嘗試獲取空閒
        
        if (wrapper == null) {
            synchronized (poolLock) {
                if (pool.size() >= param.getMaxConnection()) {
                    // 滿了,等待超時時間
                    wrapper = getFreeWrapper(param.getWaitTime());
                } else {
                    // 未滿,創建新連接
                    wrapper = createPhysicalConnection(user, password);
                }
            }
        }
        
        if (wrapper == null) 
            throw new SQLException("無可用連接,等待超時");
        
        wrapper.setInUse(true);
        return wrapper.getProxyConnection(); // 返回代理對象!
    }
    
    // 獲取空閒包裝器
    private _Connection getFreeWrapper(long timeout) throws SQLException {
        long start = System.currentTimeMillis();
        while (true) {
            synchronized (poolLock) {
                for (int i = pool.size() - 1; i >= 0; i--) { // 從尾部獲取 (LRU)
                    _Connection w = pool.get(i);
                    if (!w.isInUse()) {
                        pool.remove(i); // 移至末尾 (最近使用)
                        pool.add(w);
                        return w;
                    }
                }
            }
            
            if (timeout <= 0) return null;
            if (System.currentTimeMillis() - start >= timeout) return null;
            
            try { Thread.sleep(50); } catch (InterruptedException ignored) {}
        }
    }
    
    // 創建物理連接並包裝
    private _Connection createPhysicalConnection() throws SQLException {
        return createPhysicalConnection(param.getUser(), param.getPassword());
    }
    
    private _Connection createPhysicalConnection(String user, String password) throws SQLException {
        Connection conn = DriverManager.getConnection(param.getUrl(), user, password);
        _Connection wrapper = new _Connection(conn, true);
        pool.add(wrapper);
        return wrapper;
    }
    
    // 銷毀池:真正關閉所有物理連接
    public int close() throws SQLException {
        int closed = 0;
        SQLException firstEx = null;
        
        synchronized (poolLock) {
            for (_Connection w : pool) {
                try {
                    w.closeReal(); // 調用真實 close
                    closed++;
                } catch (SQLException e) {
                    if (firstEx == null) firstEx = e;
                }
            }
            pool.clear();
        }
        
        if (firstEx != null) throw firstEx;
        return closed;
    }
    
    // 後台清理空閒超時連接 (定時任務調用)
    public void evictExpired() {
        long now = System.currentTimeMillis();
        List<_Connection> toClose = new ArrayList<>();
        
        synchronized (poolLock) {
            Iterator<_Connection> it = pool.iterator();
            while (it.hasNext()) {
                _Connection w = it.next();
                if (!w.isInUse() && now - w.getLastAccessTime() > param.getTimeoutValue()) {
                    it.remove();
                    toClose.add(w);
                }
            }
        }
        
        for (_Connection w : toClose) {
            try { w.closeReal(); } catch (SQLException ignored) {}
        }
    }
}

三、用戶代碼:零感知使用


// 初始化 (應用啟動時一次)
ConnectionParam param = new ConnectionParam();
param.setDriver("com.mysql.cj.jdbc.Driver");
param.setUrl("jdbc:mysql://localhost:3306/mydb?useSSL=false");
param.setUser("root");
param.setPassword("123456");
param.setMinConnection(5);
param.setMaxConnection(20);
ConnectionFactory.bind("myPool", param);

// 業務代碼 (完全標準 JDBC 風格)
public void doBusiness() {
    DataSource ds = ConnectionFactory.lookup("myPool");
    
    try (Connection conn = ds.getConnection(); // 獲取代理連接
         PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {
        
        ps.setInt(1, 100);
        try (ResultSet rs = ps.executeQuery()) {
            while (rs.next()) { /* 處理結果 */ }
        }
        // conn.close() 會被攔截,連接歸還池中,不會真正關閉!
    } catch (SQLException e) {
        // 異常處理
    }
}

// 應用關閉時
ConnectionFactory.unbind("myPool"); // 真正關閉所有物理連接

四、設計亮點總結

痛點傳統方案動態代理方案
用戶調用習慣強制用 release() 替代 close()完全兼容標準 JDBC,conn.close() 照常用
狀態一致性用戶誤調 close() 導致池失控攔截 close(),池完全掌控連接生命週期
代碼侵入性業務代碼依賴池專有 API零侵入,僅依賴 javax.sql.DataSource 標準接口
擴展性難以添加監控、審計、加密InvocationHandler 可輕鬆擴展切面邏輯

五、生產環境增強建議

  1. 連接驗證:借用前執行 `SELECT 1` 或 `connection.isValid(1)` 防止斷連
  2. 準備語句緩存:在 `_Connection` 中緩存 `PreparedStatement` 提升性能
  3. 統計監控:記錄活躍/空閒/等待數,暴露 JMX 指標
  4. 異步獲取:提供 `CompletableFuture getConnectionAsync()`
  5. 多租戶隔離:ThreadLocal 綁定連接,支持事務傳播
  6. 插件化攔截器:將 `InvocationHandler` 拆分為責任鏈,支持 SQL 審計、慢查詢告警、數據脫敏

六、為什麼不用 CGLIB/字節碼操作?

java.sql.Connection 是接口,JDK 動態代理原生支持接口代理,無需第三方庫,性能開銷極低 (僅方法調用轉發),且 JDK 內置、零依賴、穩定可靠。CGLIB 針對類代理,此處不適用。

總結

動態代理模式完美解決了連接池的「習慣兼容性」與「狀態可控性」雙重難題。通過 InvocationHandler 攔截 close(),實現了邏輯關閉 (歸還池) 與物理關閉 (銷毀連接) 的分離。這是 AOP 思想在資源池化場景的經典應用——核心邏輯不變,切面行為可插拔。生產環境建議直接使用成熟池 (HikariCP, Druid),其內部原理同出一轍,更經過極致優化。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved