天天看点

mybatis日志功能是如何设计的?

引言

我们在使用

mybatis

时,如果出现sql问题,一般会把

mybatis

配置文件中的

logging.level

参数改成

debug

,这样就能在日志中看到某个

mapper

最终执行sql、入参和影响数据行数。我们拿到sql和入参,手动拼接成完整的sql,然后将该sql在数据库中执行一下,就基本能定位到问题原因。

mybatis

的日志功能使用起来还是非常方便的,大家有没有想过它是如何设计的呢?

从logging目录开始

我们先看一下

mybatis

logging

目录,该目录的功能决定了

mybatis

使用什么日志工具打印日志。

logging

目录结构如下:

mybatis日志功能是如何设计的?

它里面除了

jdbc

目录,还包含了7个子目录,每一个子目录代表一种日志打印工具,目前支持6种日志打印工具和1种非日志打印工具。我们用一张图来总结一下

mybatis日志功能是如何设计的?

除了上面的8种日志工具之外,它还抽象出一个

Log

接口,所有的日志打印工具必须实现该接口,后面可以面向接口编程。定义了

LogException

异常,该异常是日志功能的专属异常,如果你有看过

mybatis

其他源码的话,不难发现,其他功能也定义专属异常,比如:

DataSourceException

等,这是

mybatis

的惯用手法,主要是为了将异常细粒度的划分,以便更快定位问题。此外,它还定义了

LogFactory

日志工厂,以便于屏蔽日志工具实例的创建细节,让用户使用起来更简单。

如果是你该如何设计这个功能?

我们按照上面目录结构的介绍其实已经有一些思路:

  1. 定义一个

    Log

    接口,以便于统一抽象日志功能,这8种日志功能都实现

    Log

    接口,并且重写日志打印方法。
  2. LogFactory

    日志工厂,它会根据我们项目中引入的某个日志打印工具jar包,创建一个具体的日志打印工具实例。

看起来,不错。但是,再仔细想想,

LogFactory

中如何判断项目中引入了某个日志打印工具jar包才创建相应的实例呢?我们第一个想到的可能是用

if...else

判断不就行了,再想想感觉用

if...else

不好,7种条件判断太多了,并非优雅的编程。这时候,你会想一些避免太长

if...else

判断的方法,可能已经学到了几招,但是

mybatis

却用了一个新的办法。

mybatis是如何设计这个功能的?

  1. Log

    接口开始
mybatis日志功能是如何设计的?

它里面抽象了日志打印的5种方法和2种判断方法。

  1. 再分析

    LogFactory

    的代码
mybatis日志功能是如何设计的?

它里面定义了一个静态的构造器

logConstructor

,没有用

if...else

判断,在static代码块中调用了6个

tryImplementation

方法,该方法会启动一个执行任务去调用了

useXXXLogging

方法,创建日志打印工具实例。

mybatis日志功能是如何设计的?

当然

tryImplementation

方法在执行前会判断构造器

logConstructor

为空才允许执行任务中的run方法。下一步看看

useXXXLogging

方法:

mybatis日志功能是如何设计的?

看到这里,聪明的你可能会有这样的疑问,从上图可以看出

mybatis

定义了8种

useXXXLogging

方法,但是在前面的

static

静态代码块中却只调用了6种,这是为什么?

对比后发现:

useCustomLogging

useStdOutLogging

前面是没调用的。

useStdOutLogging

它里面使用了

StdOutImpl

mybatis日志功能是如何设计的?

该类其实就是通过

JDK

自带的

System

类的方法打印日志的,无需引入额外的jar包,所以不参与

static

代码块中的判断。

useCustomLogging

方法需要传入一个实现了

Log

接口的类,如果

mybatis

默认提供的6种日志打印工具不满足要求,以便于用户自己扩展。

而这个方法是在

Configuration

类中调用的,如果用户有自定义

logImpl

参数的话。

mybatis日志功能是如何设计的?
mybatis日志功能是如何设计的?

具体是在

XMLConfigBuilder

类的

settingsElement

方法中调用

mybatis日志功能是如何设计的?

再回到前面

LogFactory

setImplementation

方法

mybatis日志功能是如何设计的?

它会先找到实现了

Log

接口的类的构造器,返回将该构造器赋值给全局的

logConstructor

这样一来,就可以通过

getLog

方法获取到

Log

实例。

mybatis日志功能是如何设计的?

然后在业务代码中通过下面这种方式获取

Log

对象,调用它的方法打印日志了。

mybatis日志功能是如何设计的?

梳理一下LogFactory的流程:

  • 在static代码块中根据逐个引入日志打印工具jar包中的日志类,先判断如果全局变量logConstructor为空,则加载并获取相应的构造器,如果可以获取到则赋值给全局变量logConstructor。
  • 如果全局变量logConstructor不为空,则不继续获取构造器。
  • 根据getLog方法获取Log实例
  • 通过Log实例的具体日志方法打印日志

在这里还分享一个知识点,如果某个工具类里面都是静态方法,那么要把该工具类的构造方法定义成

private

的,防止被疑问调用,

LogFactory

就是这么做的。

mybatis日志功能是如何设计的?
  1. 适配器模式

日志模块除了使用

工厂模式

之外,还是有了

适配器模式

适配器模式会将所需要适配的类转换成调用者能够使用的目标接口

涉及以下几个角色:

  • 目标接口( Target )
  • 需要适配的类( Adaptee )
  • 适配器( Adapter)
mybatis日志功能是如何设计的?

mybatis是怎么用适配器模式的?

mybatis日志功能是如何设计的?

上图中标红的类对应的是

Adapter

角色,

Log

Target

角色。

mybatis日志功能是如何设计的?

LogFactory

就是

Adaptee

,它里面的

getLog

方法里面包含是需要适配的对象。

sql执行日志打印原理

从上面已经能够确定使用哪种日志打印工具,但在sql执行的过程中是如何打印日志的呢?这就需要进一步分析

logging

目录下的

jdbc

目录了。

mybatis日志功能是如何设计的?

看看这几个类的关系图:

mybatis日志功能是如何设计的?

ConnectionLogger

PreparedStatementLogger

ResultSetLogger

StatementLogger

都继承了

BaseJdbcLogger

类,并且实现了

InvocationHandler

接口。从类名非常直观的看出,这4种类对应的数据库jdbc功能。

mybatis日志功能是如何设计的?

它们实现了

InvocationHandler

接口意味着它用到了动态代理,真正起作用的是

invoke

方法,我们以

ConnectionLogger

为例:

mybatis日志功能是如何设计的?

如果调用了

prepareStatement

方法,则会打印debug日志。

mybatis日志功能是如何设计的?

上图中传入的

original

参数里面包含了

\n\t

等分隔符,需要将分隔符替换成空格,拼接成一行

sql

最终会在日志中打印sql、入参和影响行数:

mybatis日志功能是如何设计的?

上图中的sql语句是在ConnectionLogger类中打印的

那么入参和影响行数呢?

入参在PreparedStatementLogger类中打印的

mybatis日志功能是如何设计的?

影响行数在ResultSetLogger类中打印的

mybatis日志功能是如何设计的?

大家需要注意的一个地方是:sql、入参和影响行数只打印了debug级别的日志,其他级别并没打印。所以需要在

mybatis

logging.level

参数配置成

debug

,才能打印日志。

彩蛋

不知道大家有没有发现这样一个问题:

LogFactory

的代码中定义了很多匿名的任务执行器

mybatis日志功能是如何设计的?

但是在实际调用时,却没有在线程中执行,而是直接调用的,这是为什么?

mybatis日志功能是如何设计的?

答案是为了保证顺序执行,如果所有的日志工具jar包都有,加载优先级是:

slf4j

commonsLog

log4j2

log4j

jdkLog

NoLog

还有个问题,顺序执行就可以了,为什么要把匿名内部类定义成Runnable的呢?