MyBatis日志到底是如何做到兼容所有常用日志框架的?
点击上方蓝色“程序猿DD”,选择“设为星标”
回复“资源”获取独家整理的学习资料!
作者 | 双子孤狼
日志,在我们开发中是一个非常重要的话题,良好的日志打印可以帮助我们快速的定位问题,可能现在我们开发用到最多的日志框架就是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的日志模块也可以很明显的看出六种日志类型:它们的对应关系为:
日志属性 | 对应日志模块包名 | 实现方式 |
---|---|---|
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是如何通过动态代理将不同日志类型分为不同对象来实现日志打印功能的。
【往期推荐】
2020-11-20
2020-11-19
2020-11-18
2020-11-19
2020-11-17
扫一扫,关注我
一起学习,一起进步
每周赠书,福利不断
﹀
﹀
﹀
深度内容
推荐加入