在 Java 資料庫開發中,PreparedStatement 不僅能有效防止 SQL 注入,還能透過預編譯顯著提升查詢效能。然而,當需要記錄實際執行的 SQL 語句時,標準介面卻顯得力不從心:它只保留帶佔位符(?)的模板,無法直接輸出替換真實參數值後的完整語句。這對除錯與效能分析帶來極大不便。
最直觀的做法是在程式碼中手動拼裝 SQL 字串:
String sql = "select foo, bar from foobar where foo < ? and bar = ?";
long fooValue = 99;
String barValue = "christmas";
System.out.println("Executing query: select foo, bar from foobar where foo < " + fooValue + " and bar = '" + barValue + "'");
這種方式存在明顯缺點:
本文採用 Decorator 模式,實作一個包裝真實 PreparedStatement 的 LoggableStatement 類別。核心思路:
Connection 與 SQL 模板,建立真實 PreparedStatement 並保存模板setXxx() 方法:先委派給真實物件,再將參數值(轉為字串)存入內部列表getQueryString() 方法,將模板中的 ? 依序替換為已保存的參數值,生成可直接執行的完整 SQLpublic class LoggableStatement implements PreparedStatement {
private final PreparedStatement wrappedStatement;
private final String sqlTemplate;
private final List<Object> parameterValues = new ArrayList<>();
public LoggableStatement(Connection conn, String sql) throws SQLException {
this.wrappedStatement = conn.prepareStatement(sql);
this.sqlTemplate = sql;
}
@Override
public void setLong(int parameterIndex, long x) throws SQLException {
wrappedStatement.setLong(parameterIndex, x);
saveParameter(parameterIndex, x);
}
@Override
public void setString(int parameterIndex, String x) throws SQLException {
wrappedStatement.setString(parameterIndex, x);
saveParameter(parameterIndex, x);
}
// 其他 setXxx 方法同理...
private void saveParameter(int index, Object value) {
String strValue;
if (value instanceof String || value instanceof java.util.Date) {
strValue = "'" + value + "'";
} else if (value == null) {
strValue = "null";
} else {
strValue = value.toString();
}
// 確保列表足夠大
while (index >= parameterValues.size()) parameterValues.add(null);
parameterValues.set(index, strValue);
}
public String getQueryString() {
String result = sqlTemplate;
for (Object val : parameterValues) {
if (val != null) result = result.replaceFirst("\\?", val.toString());
}
return result;
}
// 其他介面方法直接委派給 wrappedStatement...
}
Connection conn = dataSource.getConnection();
PreparedStatement pstmt;
if (logEnabled) {
pstmt = new LoggableStatement(conn, sql);
} else {
pstmt = conn.prepareStatement(sql);
}
pstmt.setLong(1, 99);
pstmt.setString(2, "christmas");
if (logEnabled) {
System.out.println("Executing query: " + ((LoggableStatement)pstmt).getQueryString());
}
ResultSet rs = pstmt.executeQuery();
saveParameter,易於擴展這個輕量級封裝展示了「以組合代替繼承」的經典應用,不僅解決了 JDBC 日誌痛點,也為類似的介面增強提供了範本。