【MyBatis系列10】寫給大忙人看的,MyBatis日誌如何做到相容所有常用的日誌框架
MyBatis日誌原理分析
前言
日誌,在我們開發中是一個非常重要的話題,良好的日誌列印可以幫助我們快速的定位問題,可能現在我們開發用到最多的日誌框架就是slf4j了,但是日誌還有其他很多優秀的框架,比如:Apache Common Log,Log4j,java.util.logging等。MyBatis作為一款優秀的ORM框架,定義了一套統一的日誌介面供應用層呼叫,而底層卻利用介面卡模式相容了我們上面所列出來的常用日誌框架。
MyBatis日誌分類
這就說明MyBatis支援六種日誌型別(NO_LOGGING是不列印日誌)。我們看一下MyBatis的日誌模組也可以很明顯的看出六種日誌型別:
它們的對應關係為:
日誌屬性 | 對應日誌模組包名 | 實現方式 |
---|---|---|
SLF4J | slf4j | 使用SLF4J日誌框架實現 |
LOG4J | log4j | 使用Log4J日誌框架實現(1.x版本) |
LOG4J2 | log4j2 | 使用Log4J日誌框架實現(2.x版本) |
JDK_LOGGING | jdk14 | 使用java.util.logging實現 |
COMMONS_LOGGING | commons | 使用Apache Commons Logging實現 |
STDOUT_LOGGING | stdout | 使用System類實現 |
NO_LOGGING | nologging | 不列印日誌 |
PS:需要注意的是,SLF4J並不是一個具體的日誌框架,也就是我們不能單獨只配置SLF4J而不引入其他任何具體的日誌框架。
簡單談談SLF4J
SLF4J:簡單日記門面。(英文全稱為simple logging Facade for Java),這個是用來為各種日誌框架提供一個簡單的統一的介面,這樣使得我們在切換日誌框架的時候可以直接替換jar包就可以了,而無需修改原始碼。
logback我想大家都用過,logback是一個實現了具體日誌列印的框架,但是MyBatis上面列出來的分類並沒有支援logback,它又為什麼能夠列印呢?這就是SLF4J的作用了,因為logback也實現了SLF4J提供的介面,所以我們需要將logback和SLF4J結合配置使用才行。而後面的介紹中也可以看到,MyBatis中如果我們不指定日誌種類的時候,優先選擇的就是SLF4J,這正是因為SLF4J可以和其他許多日誌框架一起結合來使用。
那麼假如我們指定了日誌型別為SLF4J,但是不引入其他任何實現呢?
答案就是MyBatis不會列印任何日誌出來,下圖就是隻配置了SLF4J而沒有引入其他任何實現的警告資訊:
可以看到這裡提示我們SLF4J沒有任何實現,而後面的sql語句和引數這些資訊也沒有打印出來。
MyBatis日誌實現原理
日誌的解析
老規矩,我們還是先找到載入mybatis-config配置檔案中的解析日誌的原始碼:
這裡首先會根據我們配置的屬性作為別名去TypeAliasRegistry類中查詢對應的類,如果不存在這個別名,那就會把我們配置的屬性直接通過Class.forName去查詢日誌類,所以看到這裡就明白我們可以自定義日誌類,只要實現Log介面就行,然後配置我們自己的類名就行了。雖然別名都存在TypeAliasRegistry類裡面,但是我們前面介紹MyBatis配置檔案的時候,列出了TypeAliasRegistry類中預設初始化的別名,並沒有看到日誌相關類的別名,那麼日誌的別名又是在哪裡配置的呢?我們開啟Configuration類:
可以看到Configuration的構造方法裡面也初始化了一些別名註冊到TypeAliasRegistry類了。
接下來我們看看讀取到日誌類之後呼叫了setLogImpl做了什麼事情:
呼叫了LogFactory類的方法。
LogFactory
LogFactory工廠是負責建立日誌物件對應的介面卡。
LogFactory的靜態程式碼塊內按順序初始化了所有內建的日誌
再看一下tryImplementation方法,如果logConstructor不為空,說明當前還沒有載入到日誌介面卡,那就繼續執行run()方法,也就是繼續執行useXXXLogging方法,而所有的useXXXLogging方法都是呼叫了setImplementation方法。
下面這裡如果載入成功之後就會對logConstructor進行賦值,那麼後續的方法就不會再執行run()方法, 而如果丟擲異常,因為已經被捕獲了,所以就會繼續往後執行靜態程式碼塊內的方法。
從上面的LogFactory中我們可以看到,初始化的時候就會預設初始化一個日誌介面卡,所以如果我們引用了相關日誌所需要的類,那麼就會按照static程式碼塊內的順序進行選擇一個合適的日誌介面卡。
繼續回到上面的Configuration裡面,這裡拿到我們配置的日誌資訊之後,會直接呼叫useCustomLogging方法,也就是繞過了上面的logConstructor == null這個判斷,而直接呼叫了setImplementation方法,所以假如我們配置了日誌資訊,那麼會覆蓋初始化的日誌介面卡。
PS:假如我們配置了一個不存在的日誌類,那麼呼叫setImplementation方法的時候異常就會被丟擲來,因為捕獲異常的方法是在tryImplementation而不是在setImplementation。
jdbc log
MyBatis的日誌包下面還有一個包時jdbc,這個我們還沒有介紹,那麼jdbc包下面的類又有什麼用呢?我們先看一下類圖關係:
很明顯,MyBatis將日誌拆分成了ConnectionLogger,PreparedStatementLogger,ResultSetLogger,StatementLogger四種類型分開處理,它們都繼承了BaseJdbcLogger類,而且實現了InvocationHandler介面,也很明顯,這裡用到了JDK動態代理。
任意點開ConnectionLogger可以發現,它是用來代理Connection物件的:
其他三個那很明顯,分別是用來代理PreparedStatement,ResultSet,Statement這三個物件的。也就是說MyBatis中日誌最終的列印是通過JDK動態代理來實現的,而且不同的執行過程分成了四個物件來分別負責對應的日誌列印。
我們繼續看一下ConnectionLogger的invoke方法,可以看到,這裡就是列印了一句日誌:
上面日誌打印出來的效果就是我們下面紅框中的日誌:
等等,差點被忽悠了,這程式碼裡面並沒有列印“==>”,打印出來的這個符號又是怎麼來的呢?
那就需要進入debug方法裡面繼續看一看,這個debug方法是在抽象類BaseJdbcLogger裡面實現的,所以我們還需要看看BaseJdbcLogger類的debug方法。
可以看到這裡列印的時候拼接了一個字首:
PS:queryStack是查詢層數,如果沒有巢狀查詢則queryStack=1
總結
本文主要分析了MyBatis日誌的載入原理,並對LogFactory作為介面卡物件工廠是如何選擇日誌介面卡物件進行了分析。最後分析了MyBatis是如何通過動態代理將不同日誌型別分為不同物件來實現日誌列印功能的。
請關注我,和孤狼一起學習進步。