Mybatis日誌功能是如何設計的?

引言

咱們在使用mybatis時,若是出現sql問題,通常會把mybatis配置文件中的logging.level參數改爲debug,這樣就能在日誌中看到某個mapper最終執行sql、入參和影響數據行數。咱們拿到sql和入參,手動拼接成完整的sql,而後將該sql在數據庫中執行一下,就基本能定位到問題緣由。mybatis的日誌功能使用起來仍是很是方便的,你們有沒有想過它是如何設計的呢?程序員

從logging目錄開始

咱們先看一下mybatislogging目錄,該目錄的功能決定了mybatis使用什麼日誌工具打印日誌。算法

logging目錄結構以下:sql

它裏面除了jdbc目錄,還包含了7個子目錄,每個子目錄表明一種日誌打印工具,目前支持6種日誌打印工具和1種非日誌打印工具。咱們用一張圖來總結一下數據庫

除了上面的8種日誌工具以外,它還抽象出一個Log接口,全部的日誌打印工具必須實現該接口,後面能夠面向接口編程。定義了LogException異常,該異常是日誌功能的專屬異常,若是你有看過mybatis其餘源碼的話,不難發現,其餘功能也定義專屬異常,好比:DataSourceException等,這是mybatis的慣用手法,主要是爲了將異常細粒度的劃分,以便更快定位問題。此外,它還定義了LogFactory日誌工廠,以便於屏蔽日誌工具實例的建立細節,讓用戶使用起來更簡單。編程

若是是你該如何設計這個功能?

咱們按照上面目錄結構的介紹其實已經有一些思路:segmentfault

  1. 定義一個Log接口,以便於統一抽象日誌功能,這8種日誌功能都實現Log接口,而且重寫日誌打印方法。
  2. 定義一個LogFactory日誌工廠,它會根據咱們項目中引入的某個日誌打印工具jar包,建立一個具體的日誌打印工具實例。

看起來,不錯。可是,再仔細想一想,LogFactory中如何判斷項目中引入了某個日誌打印工具jar包才建立相應的實例呢?咱們第一個想到的多是用if...else判斷不就好了,再想一想感受用if...else很差,7種條件判斷太多了,並不是優雅的編程。這時候,你會想一些避免太長if...else判斷的方法,可是mybatis卻用了一個新的辦法。mybatis

mybatis是如何設計這個功能的?

  1. Log接口開始

它裏面抽象了日誌打印的5種方法和2種判斷方法。多線程

  1. 再分析LogFactory的代碼

它裏面定義了一個靜態的構造器logConstructor,沒有用if...else判斷,在static代碼塊中調用了6個tryImplementation方法,該方法會啓動一個執行任務去調用了useXXXLogging方法,建立日誌打印工具實例。app

固然tryImplementation方法在執行前會判斷構造器logConstructor爲空才容許執行任務中的run方法。下一步看看useXXXLogging方法:ide

看到這裏,聰明的你可能會有這樣的疑問,從上圖能夠看出mybatis定義了8種useXXXLogging方法,可是在前面的static靜態代碼塊中卻只調用了6種,這是爲何?

對比後發現:useCustomLogging 和 useStdOutLogging 前面是沒調用的。useStdOutLogging它裏面使用了StdOutImpl

該類其實就是經過JDK自帶的System類的方法打印日誌的,無需引入額外的jar包,因此不參與static代碼塊中的判斷。

useCustomLogging方法須要傳入一個實現了Log接口的類,若是mybatis默認提供的6種日誌打印工具不知足要求,以便於用戶本身擴展。

而這個方法是在Configuration類中調用的,若是用戶有自定義logImpl參數的話。

具體是在XMLConfigBuilder類的settingsElement方法中調用

再回到前面LogFactorysetImplementation方法

它會先找到實現了Log接口的類的構造器,返回將該構造器賦值給全局的logConstructor

這樣一來,就能夠經過getLog方法獲取到Log實例。

而後在業務代碼中經過下面這種方式獲取Log對象,調用它的方法打印日誌了。

梳理一下LogFactory的流程:

  • 在static代碼塊中根據逐個引入日誌打印工具jar包中的日誌類,先判斷若是全局變量logConstructor爲空,則加載並獲取相應的構造器,若是能夠獲取到則賦值給全局變量logConstructor。
  • 若是全局變量logConstructor不爲空,則不繼續獲取構造器。
  • 根據getLog方法獲取Log實例
  • 經過Log實例的具體日誌方法打印日誌

在這裏還分享一個知識點,若是某個工具類裏面都是靜態方法,那麼要把該工具類的構造方法定義成private的,防止被疑問調用,LogFactory就是這麼作的。

  1. 適配器模式

日誌模塊除了使用工廠模式以外,仍是有了適配器模式

適配器模式會將所須要適配的類轉換成調用者可以使用的目標接口

涉及如下幾個角色:

  • 目標接口( Target )
  • 須要適配的類( Adaptee )
  • 適配器( Adapter)

mybatis是怎麼用適配器模式的?

上圖中標紅的類對應的是Adapter角色,LogTarget角色。

LogFactory就是Adaptee,它裏面的getLog方法裏面包含是須要適配的對象。

sql執行日誌打印原理

從上面已經可以肯定使用哪一種日誌打印工具,但在sql執行的過程當中是如何打印日誌的呢?這就須要進一步分析logging目錄下的jdbc目錄了。

看看這幾個類的關係圖:

ConnectionLoggerPreparedStatementLoggerResultSetLoggerStatementLogger都繼承了BaseJdbcLogger類,而且實現了InvocationHandler接口。從類名很是直觀的看出,這4種類對應的數據庫jdbc功能。

類名

對應功能

ConnectionLogger

Connection

PreparedStatementLogger

PreparedStatement

ResultSetLogger

ResultSet

StatementLogger

Statement

它們實現了InvocationHandler接口意味着它用到了動態代理,真正起做用的是invoke方法,咱們以ConnectionLogger爲例:

若是調用了prepareStatement方法,則會打印debug日誌。

上圖中傳入的original參數裏面包含了nt等分隔符,須要將分隔符替換成空格,拼接成一行sql

最終會在日誌中打印sql、入參和影響行數:

上圖中的sql語句是在ConnectionLogger類中打印的

那麼入參和影響行數呢?

入參在PreparedStatementLogger類中打印的

影響行數在ResultSetLogger類中打印的

你們須要注意的一個地方是:sql、入參和影響行數只打印了debug級別的日誌,其餘級別並沒打印。因此須要在mybatis配置文件中的logging.level參數配置成debug,才能打印日誌。

彩蛋

不知道你們有沒有發現這樣一個問題:

LogFactory的代碼中定義了不少匿名的任務執行器

可是在實際調用時,卻沒有在線程中執行,而是直接調用的,這是爲何?

答案是爲了保證順序執行,若是全部的日誌工具jar包都有,加載優先級是:slf4j 》commonsLog 》log4j2 》log4j 》jdkLog 》NoLog

還有個問題,順序執行就能夠了,爲何要把匿名內部類定義成Runnable的呢?

這裏很是有迷惑性,由於它沒建立Thread類,並不會多線程執行。我我的認爲,這裏是mybatis的開發者的一種偷懶,否則須要定義一個新類代替這種執行任務的含義,還不如就用已有的。
原文:https://mp.weixin.qq.com/s/HV...

推薦閱讀

全網找不出第二份能把Mybatis講解這麼詳細且圖文並茂的PDF,開放分享

50W年薪程序員須要的技術棧分析

關於【暴力遞歸算法】你所不知道的思路

看完三件事❤️

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。

關注公衆號 『 Java鬥帝 』,不按期分享原創知識。

同時能夠期待後續文章ing🚀

相關文章
相關標籤/搜索