文章目录
前言
MyBatis定了自己的Log接口以及级别,但是并没有考虑自己去实现,而是使用第三方的日志,但是第三方日志多种多样而且日志级别也不同,MyBatis是如何对接的呢?如果当前环境中存在多个第三方的日志插件,MyBatis又该如何选择才能保证日志正常加载呢?
MyBatis日志实现
首先看一下MyBatis的日志接口
package org.apache.ibatis.logging;
public interface Log {
boolean isDebugEnabled();
boolean isTraceEnabled();
void error(String s, Throwable e);
void error(String s);
void debug(String s);
void trace(String s);
void warn(String s);
}
可以看出MyBatis的日志接口提供了四种日志级别:分别为trace、debug、warn、error。而另一个使用比较广泛的日志插件slf4j则提供了trace、debug、info、warn、error五个级别。如果现在需要兼容slf4j,需要怎么办呢?在MyBatis中,通过适配器模式解决个这个问题。
在适配器模式当中,首先有一个target目标角色,也就是期待得到的结果,在上面案例当中,就是org.apache.ibatis.logging.Log接口。然后就是Adaptee适配者角色,也就是需要被适配的接口,在上面就是org.slf4j.Logger,为了完成适配,还必须存在一个适配器角色,将待适配的接口转换成目标接口,在上面案例中就是将org.slf4j.Logger适配为org.apache.ibatis.logging.Log,这样从使用者角度看只用关注org.apache.ibatis.logging.Log接口,而无需关注org.slf4j.Logger接口。
适配器的适用场景:当调用双方都不太容易修改的时候,为了复用现有组件可以使用适配器模式;在系统接入第三方组件的时候经常被用到;注意:如果系统中存在过多的适配器,会增加系统的复杂性,设计人员应该考虑重构。
可能有人会问,为啥MyBatis不直接使用slf4j呢?其实作为一款轻量级开源组件,当然不想强制用户使用某一种日志组件了,而是应该兼容常见的日志组件,MyBatis不是仅仅兼容了slf4j,而是多种。
日志模块采用适配器模式,日志组件、适配器以及统一接口定义清晰明确符合单一职责原则;同时,客户端在使用日志时,面向Log接口编程,不需要关心底层日志模块的实现,符合依赖倒转原则,最为重要的是,如果需要加入其他第三方日志框架,只需要扩展新的模块满足新需求即可,而不需要修改原有代码,这又符合了开闭原则。
优雅加载日志组件
通过适配器模式,我们可以适配所有第三方的日志组件了,但是通常项目中只会包含其中的一个或多个,甚至没有,此时如何加载这些日志呢?也就是该使用上面这些适配器中的哪个作为Log的实现呢?对应的实现类为org.apache.ibatis.logging.LogFactory。在这个类中存在以下的静态块。
static {
tryImplementation(() -> useSlf4jLogging());
tryImplementation(() -> useCommonsLogging());
tryImplementation(() -> useLog4J2Logging());
tryImplementation(() -> useLog4JLogging());
tryImplementation(() -> useJdkLogging());
tryImplementation(() -> useNoLogging());
}
对应的方法为
private static Constructor<? extends Log> logConstructor;
<font face="Consolas" color=#151663 >无非就是执行目标方法
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
而这些方法都是调用了setImplementation
private static void setImplementation(Class<? extends Log> implClass) {
try {
1. 获取构造器
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
2. 反射创建适配器对象
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
3. 设置logConstructor属性
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
这些任务无非就是通过适配器构造反射出适配器对象并设置到logConstructor属性当中,如果一切正常,因为logConstructor已经有值了,后续的日志组件都不会再加载了,如果抛出了异常(一般就是被适配的日志组件包不存在),会在tryImplementation捕捉但不处理,logConstructor也不会有值,继续加载下一个适配器,因为其实在静态块中定义了加载日志组件的优先级别为useSlf4jLogging->useCommonsLogging->useLog4J2Logging->useLog4JLogging->useJdkLogging,即使没有任何日志组件,也没有关系,useNoLogging,即不打印日志。
总结
MyBatis的日志模块使用了适配器模式解决必须兼容多种不同日志组件的目的,同时通过一个静态块优雅的加载日志组件。