曆史文章:
Mybatis 源碼學習(10)-類型轉換(TypeAliasRegistry)
Java 開發中常用的日志工具類包括Log4J、Log4J2、Apache Common Log、java.util.logging、Slf4j,這些工具的接口并不統一,為了提供統一的接口,Mybatis 對這些日志接口做了統一适配。
Mybatis 的日志子產品使用了擴充卡模式,其内部提供了統一的擴充卡接口:org.apache.ibatis.logging.Log,通過實作對接不同的第三方日志元件,實作多個 Adapter,進而将第三方元件适配成 org.apache.ibatis.logging.Log 對象,這樣 Mybatis 内部就可以統一通過 org.apache.ibatis.logging.Log 接口調用第三方元件。
日志擴充卡
前面已經提到,第三方日志元件各自具有自己的日志級别,如 java.util.logging 包含 ALL、FINEST、FINER、FINE、CONFIG、INFO、WARNING、SEVERE、OFF 這 9 種日志級别,而 Log4j2 則隻有 trace、debug、info、warn、error、fatal 這 6 種日志級别。Mybatis 僅支援 trace、debug、warn、error 這四種級别的日志。
Mybatis 的日志子產品位于 org.apache.ibatis.logging 包内,通過 Log 接口對外提供功能,LogFactory 工廠類對外提供日志元件适配功能。
LogFactory 工廠類,使用其内部的靜态代碼初識化日志擴充卡,并使用 LogFactory.logConstructor 記錄首個适配成功的日志擴充卡。
// 記錄第一個搜尋到的日志元件的構造器
private static Constructor<? extends Log> logConstructor;
static {
// 針對每種日志元件嘗試加載,預設順序是:
// useSlf4jLogging ->useCommonsLogging -> useLog4J2Logging ->
// useLog4JLogging -> useJdkLogging -> useNoLogging
tryImplementation(new Runnable() {
@Override
public void run() {
useJdkLogging();
}
});
// … 省略其他方法
}
LogFactory.tryImplementation
方法會檢查是否已查找到可以日志擴充卡,如果尚未找到,則使用 Runnable.run 嘗試加載對應的日志擴充卡,嘗試失敗則忽略異常,嘗試成功則記錄下該日志擴充卡的構造器。
// 嘗試加載
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) { // 未找到時,才允許加載
try {
runnable.run();
} catch (Throwable t) {
// 忽略所有異常
}
}
}
// 加載 Slf4
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
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(“…”);
}
// 如果加載正常,則初始化 logConstructor
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException(“…”);
}
}
Jdk14LoggingImpl 實作了 org.apache.ibatis.logging.Log 接口,并封裝了 java.util.logging.Logger,所有的日志操作,均由 java.util.logging.Logger 實作。
public class Jdk14LoggingImpl implements Log {
private final Logger log; // 封裝 java.util.logging.Logger 對象
public Jdk14LoggingImpl(String clazz) {
log = Logger.getLogger(clazz); // 初始化 java.util.logging.Logger 對象
}
// 所有的日志請求都由 java.util.logging.Logger 進行操作
@Override
public void debug(String s) {
log.log(Level.FINE, s);
}
@Override
public void warn(String s) {
log.log(Level.WARNING, s);
}
// … 忽略其他方法
}
其他幾個日志擴充卡的邏輯和 Jdk14LoggingImpl 的邏輯類似。
JDBC 日志功能
除了基本的日志适配外,Mybatis 還提供了 org.apache.ibatis.logging.jdbc 包,這個包提供了 SQL 日志調試功能,可以列印 SQL 語句、SQL 參數、結果集的内容以及行數。該目錄下的 BaseJdbcLogger 是所有 SQL 日志類的基類,内部定義了 SET_METHODS、EXECUTE_METHODS 兩個集合,用于記錄 PreparedStatement 執行的方法和設定參數的方法(set* 方法)。
BaseJdbcLogger 内的兩個集合的定義:
// PreparedStatement 接口定義的 set*() 方法集合
protected static final Set<String> SET_METHODS = new HashSet<String>();
// 記錄 Statement 和 PreparedStatement 執行 SQL 的方法集合
protected static final Set<String> EXECUTE_METHODS = new HashSet<String>();
// 初始化兩個集合
static {
SET_METHODS.add("setString");
SET_METHODS.add("setNString");
SET_METHODS.add("setInt");
// … 省略其他 set*() 方法
EXECUTE_METHODS.add("execute");
EXECUTE_METHODS.add("executeUpdate");
EXECUTE_METHODS.add("executeQuery");
EXECUTE_METHODS.add("addBatch");
}
BaseJdbcLogger 的核心字段定義如下:
// 記錄 PreparedStatement.set*() 方法設定的鍵值對
private final Map<Object, Object> columnMap = new HashMap<Object, Object>();
// 記錄 PreparedStatement.set*() 方法設定的 key
private final List<Object> columnNames = new ArrayList<Object>();
// 記錄 PreparedStatement.set*() 方法設定的 value
private final List<Object> columnValues = new ArrayList<Object>();
BaseJdbcLogger 提供的工具方法會在後邊繼續分析,繼續分析 ConnectionLogger,ConnectionLogger 是 BaseJdbcLogger 的子類,也是基于 JDK 的代理,它實作了 InvocationHandler 接口。ConnectionLogger.newInstance 方法會封裝對應的 Connection 對象,并建立對應的代理對象 —— ConnectionLogger。
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
// 傳入 Connection 對象,并建立 InvocationHandler
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
// 建立 Connection 的代理對象
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
ConnectionLogger.invoke 是實際執行 Connection 方法的具體實作,它會攔截 prepareStatement、prepareCall、createStatement 方法并執行實際的 Connection 的方法。
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);
}
}
PreparedStatementLogger 也繼承自 BaseJdbcLogger,并實作了 InvocationHandler,它的 newInstance 方法也建立了代理對象。PreparedStatementLogger.invoke 方法會為 EXECUTE_METHODS 集合、SET_METHODS 集合、getResultSet 方法、getUpdateCount 方法進行代理。
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
// 直接調用 Object 的方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
// 調用執行類方法
if (EXECUTE_METHODS.contains(method.getName())) {
if (isDebugEnabled()) {
debug("Parameters: " + getParameterValueString(), true);
}
clearColumnInfo(); // 清空 BaseJdbcLogger 中記錄的三個參數集合
if ("executeQuery".equals(method.getName())) {
// 如果執行的是 executeQuery 則為 ResultSet 建立代理對象
// 其他 execute* 方法的傳回值不是 ResultSet
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else {
// 其他情況,直接傳回結果
return method.invoke(statement, params);
}
} else if (SET_METHODS.contains(method.getName())) {
// 調用 SET_METHODS 中記錄的 set*() 方法設定參數,則需要通過 setColumn 方法記錄對應的參數
if ("setNull".equals(method.getName())) {
setColumn(params[0], null);
} else {
setColumn(params[0], params[1]);
}
return method.invoke(statement, params);
} else if ("getResultSet".equals(method.getName())) {
// 如果是 getResultSet 方法,則建立 ResultSet 代理
ResultSet rs = (ResultSet) method.invoke(statement, params);
return rs == null ? null : ResultSetLogger.newInstance(rs, statementLog, queryStack);
} else if ("getUpdateCount".equals(method.getName())) {
// 如果調用 getUpdateCount 方法,則直接輸出更新影響的行數
int updateCount = (Integer) method.invoke(statement, params);
if (updateCount != -1) {
debug(" Updates: " + updateCount, false);
}
return updateCount;
} else {
return method.invoke(statement, params);
}
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
StatementLogger 和 PreparedStatementLogger 的實作類似,但是它不允許設定參數,是以不會攔截 SET_METHODS 記錄的設定參數的方法。
ResultSetLogger 封裝了 ResultSet,也繼承自 BaseJdbcLogger,實作了 InvocationHandler。它的主要字段如下:
private boolean first = true; // 是否是 ResultSet 的第一行
private int rows; // 影響行數
private final ResultSet rs; // 實際的 ResultSet
private final Set<Integer> blobColumns = new HashSet<Integer>(); // 記錄超長字段的下标
// 長度超長的字段
private static Set<Integer> BLOB_TYPES = new HashSet<Integer>();
static {
BLOB_TYPES.add(Types.BINARY);
// … 添加其他類型:Types.BLOB、Types.CLOB、Types.LONGNVARCHAR、
// Types.LONGVARBINARY、Types.LONGVARCHAR、Types.NCLOB、Types.VARBINARY
}
ResultSetLogger.newInstance 也隻是在建立代理對象,ResultSetLogger.invoke 方法會攔截 ResultSet 的 next() 方法,并解析列名和參數值,輸出到日志裡。
public Object invoke(Object proxy, Method method, Object[] params) throws Throwable {
try {
// 直接轉發繼承自 Object 的方法
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, params);
}
Object o = method.invoke(rs, params);
// 對 next 方法進行攔截
if ("next".equals(method.getName())) {
if (((Boolean) o)) { // 是否存在下一行
rows++;
if (isTraceEnabled()) {
ResultSetMetaData rsmd = rs.getMetaData(); // 擷取表頭
final int columnCount = rsmd.getColumnCount(); // 擷取行數
if (first) {
first = false;
// 輸出表頭,并将超長字段記錄到 blobColumns 中
printColumnHeaders(rsmd, columnCount);
}
// 輸出本行内容,但是特殊展示超長字段
printColumnValues(columnCount);
}
} else { // 列印總共影響的行數
debug(" Total: " + rows, false);
}
}
clearColumnInfo(); // 清除 BaseJdbcLogger 記錄的參數集合
return o;
} catch (Throwable t) {
throw ExceptionUtil.unwrapThrowable(t);
}
}
總結
日志擴充卡的邏輯不算複雜,它使用 LogFactory 作為工廠類建立對應的日志擴充卡,而 Log 類提供了日志擴充卡的接口。在進行日志加載時,是按照既定的順序執行的,即如果查找到第一個可用擴充卡時,就不會使用後面的擴充卡了。至于為什麼嘗試加載使用 Runnable.run,這是為了友善,不再新定義操作接口。另外,如果 LogFactory 加載了 classpath 中沒有的日志類,tryImplementation 會忽略掉錯誤,繼續加載後邊的日志擴充卡。
JDBC 類型的日志關鍵在于為每種 JDBC 操作對象建立 InvocationHandler 代理對象,代理對象會攔截每個操作,并根據方法名稱和操作結果生成對應日志資訊。
參考文檔:《Mybatis 技術内幕》
本文的基本脈絡參考自《Mybatis 技術内幕》,編寫文章的原因是希望能夠系統地學習 Mybatis 的源碼,但是如果僅閱讀源碼或者僅從官方文檔很難去系統地學習,是以希望參考現成的文檔,按照文章的脈絡逐漸學習。
歡迎關注我的公衆号:我的搬磚日記,我會定時分享自己的學習曆程。