Mybatis 調試輸出SQL語句,到底是如何實現的呢?
Java 開發中常用的幾款日志框架有很多種,并且這些日志框架來源于不同的開源組織,給用戶暴露的接口也有很多不同之處,所以很多開源框架會自己定義一套統一的日志接口,兼容上述第三方日志框架,供上層使用。
一般實現的方式是使用適配器模式,將各個第三方日志框架接口轉換為框架內部自定義的日志接口。MyBatis 也提供了類似的實現,這里我們就來簡單了解一下。
適配器模式是什么?
簡單來說,適配器模式主要解決的是由于接口不能兼容而導致類無法使用的問題,這在處理遺留代碼以及集成第三方框架的時候用得比較多。其核心原理是:通過組合的方式,將需要適配的類轉換成使用者能夠使用的接口。
日志模塊
MyBatis 自定義的 Log 接口位于 org.apache.ibatis.logging 包中,相關的適配器也位于該包中。
首先是 LogFactory 工廠類,它負責創建 Log 對象,在 LogFactory 類中有一段靜態代碼塊,其中會依次加載各個第三方日志框架的適配器。
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
以 JDK Logging 的加載流程(useJdkLogging() 方法)為例,其具體代碼實現和注釋如下:
/**
* 首先會檢測 logConstructor 字段是否為空,
* 1.如果不為空,則表示已經成功確定當前使用的日志框架,直接返回;
* 2.如果為空,則在當前線程中執行傳入的 Runnable.run() 方法,嘗試確定當前使用的日志框架
*/
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
public static synchronized void useJdkLogging() {
setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
}
private static void setImplementation(Class implClass) {
try {
// 獲取適配器的構造方法
Constructor candidate = implClass.getConstructor(String.class);
// 嘗試加載適配器,加載失敗會拋出異常
Log log = candidate.newInstance(LogFactory.class.getName());
// 加載成功,則更新logConstructor字段,記錄適配器的構造方法
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
打印SQL語句
如何開啟打印
這里演示Mybatis在運行時怎么輸出SQL語句,具體分析見原理章節。
單獨使用Mybatis
在mybatis.xml配置文件中添加如下配置:
<setting name="logImpl" value="STDOUT_LOGGING" />
和SpringBoot整合
有兩種方式,第一種也是利用StdOutImpl實現類去實現打印,在application.yml配置文件填寫如下:
#mybatis配置 mybatis: # 控制臺打印sql日志 configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
其次我們還可以通過指定日志級別來輸出SQL語句:
SpringBoot默認使用的SL4J(日志門面)+Logback(具體實現)的日志組合
logging: level: xx包名: debug
簡單分析原理
這里我們直接看到org.apache.ibatis.executor.BaseExecutor#getConnection方法,了解Mybatis的應該都知道Mybatis在執行sql操作的時候會去獲取數據庫連接
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
// 判斷日志級別是否為Debug,是的話返回代理對象
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
可以看到我注釋的那行,它通過判斷日志級別來判斷是否返回ConnectionLogger代理對象,那么我們前面提到 Log 接口的實現類中StdOutImpl它的isDebugEnabled其實是永遠返回 true,代碼如下:
并且它直接用的 System.println去輸出的SQL信息
public class StdOutImpl implements Log {
// ...省略無關代碼
@Override
public boolean isDebugEnabled() {
return true;
}
@Override
public boolean isTraceEnabled() {
return true;
}
@Override
public void error(String s, Throwable e) {
System.err.println(s);
e.printStackTrace(System.err);
}
@Override
public void error(String s) {
System.err.println(s);
}
// ...省略無關代碼
}
到這里起碼你知道了為什么我們通過配置 MyBatis 所用日志的具體實現 logImpl就可以實現日志輸出到控制臺的效果了。
那么我們還可以深究一下 statementLog 是在什么時候變成 StdOutImpl的,在解析Mybatis配置文件的時候,會去讀取我們配置的logImpl屬性,然后通過LogFactory.useCustomLogging方法先指定好適配器的構造方法
// org.apache.ibatis.builder.xml.XMLConfigBuilder#loadCustomLogImpl
private void loadCustomLogImpl(Properties props) {
Class logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
public void setLogImpl(Class logImpl) {
if (logImpl != null) {
this.logImpl = logImpl;
LogFactory.useCustomLogging(this.logImpl);
}
}
然后在構建MappedStatement的時候就已經將日志對象初始化好了
每個MappedStatement對應了我們自定義Mapper接口中的一個方法,它保存了開發人員編寫的SQL語句、參數結構、返回值結構、Mybatis對它的處理方式的配置等細節要素,是對一個SQL命令是什么、執行方式的完整定義。
public Builder(Configuration configuration, String id, SqlSource sqlSource, SqlCommandType sqlCommandType) {
// ...省略無關代碼
mappedStatement.statementLog = LogFactory.getLog(logId);
mappedStatement.lang = configuration.getDefaultScriptingLanguageInstance();
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
最后SpringBoot的就不概述了
- 第一種方式其實也是同理
- 第二種方式是通過修改了日志級別,然后使
isDebugEnabled返回true,去返回代理對象,然后去輸出SQL語句。
感興趣的還可以看看SQL語句的輸出是怎么輸出的,具體在 ConnectionLogger的invoke方法中,你會發現熟悉的Preparing: "和"Parameters: "。