一.日誌模組
首先日誌在我們開發過程中佔據了一個非常重要的地位,是開發和運維管理之間的橋樑,在Java中的日誌框架也非常多,Log4j,Log4j2,Apache Commons Log,java.util.logging,slf4j等,這些工具對外的介面也都不盡相同,為了統一這些工具,MyBatis定義了一套統一的日誌介面供上層使用。如果要看懂首先對於介面卡模式要了解下
1.1 Log
Log介面中定義了四種日誌級別,相比較其他的日誌框架的多種日誌級別顯得非常的精簡,但也能夠滿足大多數常見的使用了
public interface Log { boolean isDebugEnabled(); boolean isTraceEnabled(); void error(String s, Throwable e); void error(String s); void debug(String s); void trace(String s); void warn(String s); }
1.2 LogFactory
LogFactory工廠類負責建立日誌元件介面卡,通過這玩意能找到具體的實現
在LogFactory類載入時會執行其靜態程式碼塊,其邏輯是按序載入並例項化對應日誌元件的介面卡,然後使用LogFactory.logConstructor這個靜態欄位,記錄當前使用的第三方日誌元件的介面卡。具體程式碼如下
1.3 日誌應用
如果想知道在MyBatis系統啟動的時候日誌框架是如何選擇的,那麼首先要在全域性配置檔案中我們可以設定對應的日誌型別選擇
在Configuration的構造方法中其實是設定的各個日誌實現的別名的,其中STDOUT_LOGGING這個也可以在裡面找到
然後在解析全域性配置檔案的時候就會處理日誌的設定
進入方法
private void loadCustomLogImpl(Properties props) {
// 獲取 logImpl設定的 日誌 型別
Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
// 設定日誌
configuration.setLogImpl(logImpl);
}
進入setLogImpl方法中
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl; // 記錄日誌的型別
// 設定 適配選擇
LogFactory.useCustomLogging(this.logImpl);
}
}
再進入useCustomLogging方法
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
private static void setImplementation(Class<? extends Log> implClass) {
try {
// 獲取指定介面卡的構造方法
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
// 例項化介面卡
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
// 初始化 logConstructor 欄位
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
這就關聯上了前面在LogFactory中看到的程式碼,啟動測試方法看到的日誌也和原始碼中的對應上來了,還有就是我們自己設定的會覆蓋掉預設的sl4j日誌框架的配置
1.4 JDBC 日誌
當開啟了 STDOUT的日誌管理後,當執行SQL操作時會發現在控制檯中可以打印出相關的日誌資訊
那這些日誌資訊是怎麼打印出來的呢?其實在MyBatis中的日誌模組中包含了一個jdbc包,它並不是將日誌資訊通過jdbc操作儲存到資料庫中,而是通過JDK動態代理的方式,將JDBC操作通過指定的日誌框架打印出來。下面就來看看它是如何實現的。
1.4.1 BaseJdbcLogger
BaseJdbcLogger是一個抽象類,它是jdbc包下其他Logger的父類。繼承關係如下
從圖中也可以看到4個實現都實現了InvocationHandler介面。屬性含義如下
// 記錄 PreparedStatement 介面中定義的常用的set*() 方法
protected static final Set<String> SET_METHODS;
// 記錄了 Statement 介面和 PreparedStatement 介面中與執行SQL語句有關的方法
protected static final Set<String> EXECUTE_METHODS = new HashSet<>(); // 記錄了PreparedStatement.set*() 方法設定的鍵值對
private final Map<Object, Object> columnMap = new HashMap<>();
// 記錄了PreparedStatement.set*() 方法設定的鍵 key
private final List<Object> columnNames = new ArrayList<>();
// 記錄了PreparedStatement.set*() 方法設定的值 Value
private final List<Object> columnValues = new ArrayList<>(); protected final Log statementLog;// 用於日誌輸出的Log物件
protected final int queryStack; // 記錄了SQL的層數,用於格式化輸出SQL
1.4.2 ConnectionLogger
ConnectionLogger的作用是記錄資料庫連線相關的日誌資訊,在實現中是建立了一個Connection的代理物件,在每次Connection操作的前後都可以實現日誌的操作。
public final class ConnectionLogger extends BaseJdbcLogger implements InvocationHandler { // 真正的Connection物件
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繼承過來的方法,就直接呼叫 toString,hashCode,equals等
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// 如果呼叫的是 prepareStatement方法
if ("prepareStatement".equals(method.getName())) {
if (isDebugEnabled()) {
debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true);
}
// 建立 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();
// 建立了 Connection的 代理物件 目的是 增強 Connection物件 給他添加了日誌功能
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
} /**
* return the wrapped connection.
*
* @return the connection
*/
public Connection getConnection() {
return connection;
} }
其他幾個xxxxLogger的實現和ConnectionLogger幾乎是一樣的就不重複說了,看懂一個就可以看懂所有
1.4.3 應用實現
在實際處理的時候,看下日誌模組是如何工作的,在我們要執行SQL語句前需要獲取Statement物件,而Statement物件是通過Connection獲取的,所以在SimpleExecutor中就可以看到相關的程式碼
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Statement stmt;
Connection connection = getConnection(statementLog);
// 獲取 Statement 物件
stmt = handler.prepare(connection, transaction.getTimeout());
// 為 Statement 設定引數
handler.parameterize(stmt);
return stmt;
}
先進入如到getConnection方法中
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
// 建立Connection的日誌代理物件
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
再進入到handler.prepare方法中
@Override
public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
ErrorContext.instance().sql(boundSql.getSql());
Statement statement = null;
try {
statement = instantiateStatement(connection);
setStatementTimeout(statement, transactionTimeout);
setFetchSize(statement);
return statement;
} catch (SQLException e) {
closeStatement(statement);
throw e;
} catch (Exception e) {
closeStatement(statement);
throw new ExecutorException("Error preparing statement. Cause: " + e, e);
}
}
@Override
protected Statement instantiateStatement(Connection connection) throws SQLException {
String sql = boundSql.getSql();
if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
String[] keyColumnNames = mappedStatement.getKeyColumns();
if (keyColumnNames == null) {
return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
} else {
// 在執行 prepareStatement 方法的時候會進入進入到ConnectionLogger的invoker方法中
return connection.prepareStatement(sql, keyColumnNames);
}
} else if (mappedStatement.getResultSetType() == ResultSetType.DEFAULT) {
return connection.prepareStatement(sql);
} else {
return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
}
}
在執行sql語句的時候
如果是查詢操作,後面的ResultSet結果集操作,其他是也通過ResultSetLogger來處理的