上一章的案例,配置日誌級別爲 debug,執行一個簡單的查詢操作,會將 JDBC 操作打印出來。本章通過 MyBatis 日誌部分源碼分析它是如何實現日誌打印的。
在 MyBatis 的日誌模塊中有一個 jdbc package,package 中的內容如下圖所示:
BaseJdbcLogger 是一個抽象類,它是 jdbc package 下其他類的父類,類繼承關係如下圖所示:
BaseJdbcLogger 類中定義了一些公共集合和簡單的工具方法,提供給子類使用。
BaseJdbcLogger 的子類有如下特性:
- ConnectionLogger:Connection 的代理類,封裝了 Connection 對象,繼承了 BaseJdbcLogger 抽象類並實現了 InvocationHandler 接口,newInstance() 方法會爲其封裝的 Connection 對象創建相應的代理對象;
- PreparedStatementLogger:PreparedStatement 的代理類,封裝了 PreparedStatement 對象,繼承了 BaseJdbcLogger 抽象類並實現了 InvocationHandler 接口,newInstance() 方法的實現與 Connection 的類似;
- StatementLogger:與 PreparedStatementLogger 類似;
- ResultSetLogger:ResultSet 的代理類,封裝了 ResultSet 對象,繼承了 BaseJdbcLogger 抽象類並實現了 InvocationHandler 接口,newInstance() 方法的實現與 Connection 的類似;
MyBatis 就是通過動態代理的方式,對 JDBC 原生類進行了一層封裝,在代理類的 invoke 方法中添加對應 JDBC 操作的日誌打印功能。
ConnectionLogger 的實現如下:
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { private final Connection connection; private ConnectionLogger(Connection conn, Log statementLog, int queryStack) { super(statementLog, queryStack); this.connection = conn; } @Override public Object invoke(Object proxy, Method method, Object[] params) throws Throwable { try { // 如果調用的是從Object繼承的方法,則直接調用,不做任何其他處理 if (Object.class.equals(method.getDeclaringClass())) { return method.invoke(this, params); } // 如果調用的是prepareStatement()方法、prepareCall()方法或createStatement()方法 // 則在創建相應的statement對象後,爲其創建代理對象並返回該代理對象 if ("prepareStatement".equals(method.getName())) { // 輸出日誌 if (isDebugEnabled()) { debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); } // 調用Connection的prepareStatement()方法,得到PreparedStatement對象 PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); // 爲PreparedStatement對象創建代理對象 stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if ("prepareCall".equals(method.getName())) { // 輸出日誌 if (isDebugEnabled()) { debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); } PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else if ("createStatement".equals(method.getName())) { Statement stmt = (Statement) method.invoke(connection, params); stmt = StatementLogger.newInstance(stmt, statementLog, queryStack); return stmt; } else { return method.invoke(connection, params); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } } /* * Creates a logging version of a connection * * @param conn - the original connection * @return - the connection with logging */ public static Connection newInstance(Connection conn, Log statementLog, int queryStack) { // 使用動態代理的方式創建代理對象 InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack); ClassLoader cl = Connection.class.getClassLoader(); return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler); } /* * return the wrapped connection * * @return the connection */ public Connection getConnection() { return connection; } }
其他子類的實現與 ConnectionLogger 類似,不在贅述。
ConnectionLogger 會創建 PreparedStatementLogger 或 StatementLogger,PreparedStatementLogger 會創建 ResultSetLogger,這樣就保證了每一步 JDBC 操作在 debug 日誌級別下都有日誌輸出。
那麼 ConnectionLogger 又是在哪裏創建的呢?跟蹤 SQL 的執行流程,在 org.apache.ibatis.executor.BaseExecutor#getConnection 方法中找到 ConnectionLogger 的創建代碼:
protected Connection getConnection(Log statementLog) throws SQLException { Connection connection = transaction.getConnection(); // 判斷日誌級別爲debug,則創建Connection的代理類ConnectionLogger if (statementLog.isDebugEnabled()) { return ConnectionLogger.newInstance(connection, statementLog, queryStack); } else { return connection; } }
從源碼中可以看出,如果日誌級別爲 debug,則會創建代理類 ConnectionLogger,否則只會使用正常的 Connection 對象。
MyBatis 源碼篇