点击上方 一个优秀的废人,选择 设为星标
优质文章,及时送达
巨人的肩膀:blog.csdn.net/zwx900102/article/details/109025846
前言
日志,在我们开发中是一个非常重要的话题,良好的日志打印可以帮助我们快速的定位问题,可能现在我们开发用到最多的日志框架就是 slf4j 了,但是日志还有其他很多优秀的框架,比如:Apache Common Log,Log4j,java.util.logging 等。
MyBatis 作为一款优秀的 ORM 框架,定义了一套统一的日志接口供应用层调用,而底层却利用适配器模式兼容了我们上面所列出来的常用日志框架。
MyBatis 日志分类
在介绍 MyBatis 的全局配置文件的时候,我们提到 setting 内有一个属性 logImpl,可以配置的选项有:SLF4J,LOG4J,LOG4J2,JDK_LOGGING,COMMONS_LOGGING,STDOUT_LOGGING,NO_LOGGING。
这就说明 MyBatis 支持六种日志类型 (NO_LOGGING 是不打印日志)。我们看一下 MyBatis 的日志模块也可以很明显的看出六种日志类型:

它们的对应关系为:
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 是如何通过动态代理将不同日志类型分为不同对象来实现日志打印功能的。
-END-
如果看到这里,喜欢这篇文章的话,请帮点个好看。微信搜索「一个优秀的废人」,关注后回复「 1024」送你一套完整的 java 教程(包括视频)。回复「 电子书」送你全编程领域电子书 (不只Java)。

本文分享自微信公众号 - 一个优秀的废人(feiren_java)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。
来源:oschina
链接:https://my.oschina.net/nasus/blog/4720337