傳統連接池迫使用戶:
本文展示如何利用 JDK 動態代理 (Proxy + InvocationHandler) 完美解決這兩個問題:用戶代碼零修改,透明復用連接。
[用戶代碼] ──► DataSource.getConnection()
│ │
│ ▼
│ [DriverManager.getConnection()] ──► [真實 Connection]
│ │
│ ▼
│ [_Connection 包裝器] ──實現 InvocationHandler
│ │
│ ▼
│ [Proxy.newProxyInstance()] ──► [代理 Connection] ◄── 返回用戶
│
▼
[用戶調用 proxy.close()]
│
▼
[InvocationHandler.invoke() 攔截]
│
├── 非 close 方法 ──► 轉發真實 Connection
│
└── close 方法 ──► 標記連接空閒 (不關閉真實連接) ──► 返回
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...
}
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);
}
}
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; }
}
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 可輕鬆擴展切面邏輯 |
java.sql.Connection 是接口,JDK 動態代理原生支持接口代理,無需第三方庫,性能開銷極低 (僅方法調用轉發),且 JDK 內置、零依賴、穩定可靠。CGLIB 針對類代理,此處不適用。
動態代理模式完美解決了連接池的「習慣兼容性」與「狀態可控性」雙重難題。通過 InvocationHandler 攔截 close(),實現了邏輯關閉 (歸還池) 與物理關閉 (銷毀連接) 的分離。這是 AOP 思想在資源池化場景的經典應用——核心邏輯不變,切面行為可插拔。生產環境建議直接使用成熟池 (HikariCP, Druid),其內部原理同出一轍,更經過極致優化。