简述
在Java开发中常用的日志框架有Log4j、Log4j2、Apache Commons Log、java.util.logging、slf4j等,这些工具对外的接口并不相同。为了统一这些工具的接口,MyBatis定义了一套统一的日志接口供上层使用,并为上述常用的日志框架提供了相应的适配器。
适配器模式
首先,我们简单介绍设计模式中有六大原则。
单一职责原则: 不要存在多于一个导致类变更的原因,简单来说,一个类只负责唯一项职责。
里氏替换原则: 如果对每一个类型为T1的对象t1,都有类型为T2的对象t2,使得以T1定义的所有程序P在所有的对象t1都代换成t2时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。遵守里氏替换原则,可以帮助我们设计出更为合理的继承体系。
依赖倒置原则: 系统的高层模块不应该依赖低层模块的具体实现,二者都应该依赖其抽象类或接口,抽象接口不应该依赖具体实现类,而具体实现类应该于依赖抽象。简单来说,我们要面向接口编程。当需求发生变化时对外接口不变,只要提供新的实现类即可。
接口隔离原则: 一个类对另一个类的依赖应该建立在最小的接口上。简单来说,我们在设计接口时,不要设计出庞大臃肿的接口,因为实现这种接口时需要实现很多不必要的方法。我们要尽量设计出功能单一的接口,这样也能保证实现类的职责单一。
迪米特法则: 一个对象应该对其他对象保持最少的了解。简单来说,就是要求我们减低类间耦合。
开放-封闭原则: 程序要对扩展开放,对修改关闭。简单来说,当需求发生变化时,我们可以通过添加新的模块满足新需求,而不是通过修改原来的实现代码来满足新需求。
在这六条原则中,开放-封闭原则是最基础的原则,也是其他原则以及后文介绍的所有设计模式的最终目标。
适配器模式的主要目的是解决由于接口不能兼容而导致类无法使用的问题,适配器模式会将需要适配的类转换成调用者能够使用的目标接口。这里先介绍适配器模式中涉及的几个角色:
- 目标接口(Target):调用者能够直接使用的接口。
- 源(Adaptee)角色:需要适配的接口
- 适配器(Adapter)角色:适配器类是本模式的核心。适配器把源接口转换成目标接口。显然,这一角色不可以是接口,而必须是具体类。
使用适配器模式的好处就是复用现有组件。应用程序需要复用现有的类,但接口不能被该应用程序兼容,则无法直接使用。这种场景下就适合使用适配器模式实现接口的适配,从而完成组件的复用。很明显,适配器模式通过提供Adapter的方式完成接口适配,实现了程序复用Adaptee的需求,避免了修改Adaptee实现接口,这符合“开放-封闭”原则。当有新的Adaptee需要被复用时,只要添加新的Adapter即可,这也是符合“开放-封闭”原则的。
在MyBatis的日志模块中,就使用了适配器模式。Log4j、Log4j2等第三方日志组件对外提供的接口各不相同,MyBatis为了集成和复用这些第三方日志组件,在其日志模块中提供了多种Adapter,将这些第三方日志组件对外的接口适配成了org.apache.ibatis.logging.Log接口,这样MyBatis内部就可以统一通过org.apache.ibatis.logging.Log接口调用第三方日志组件的功能了。
日志适配器
前面提到的多种第三方日志组件都有各自的Log级别,且都有所不同,例如java.util.logging提供了All、FINEST、FINER、FINE、CONFIG、INFO、WARNING等9种级别,而Log4j2则只有trace、debug、info、warn、error、fatal这6种日志级别。MyBatis统一提供了trace、debug、warn、error四个级别,这基本与主流日志框架的日志级别类似,可以满足绝大多数场景的日志需求。 MyBatis的日志模块位于org.apache.ibatis.logging包中,该模块中通过Log接口定义了日志模块的功能,当然日志适配器也会实现此接口。LogFactory工厂类负责创建对应的日志组件适配器
在LogFactory类加载时会执行其静态代码块,其逻辑是按序加载并实例化对应日志组件的适配器,然后使用LogFactory.logConstructor这个静态字段,记录当前使用的第三方日志组件的适配器,
/**
* 记录当前使用的第三方日志组件所对应的适配器的构造方法
*/
private static Constructor<? extends Log> logConstructor;
/**
* 调用方法tryImplementation顺序加载每种组件
*/
static {
tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useLog4J2Logging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useLog4JLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useJdkLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useNoLogging();
}
});
}
LogFactory.tryImplementation()方法首先会检测logConstructor字段,若为空则调用Runnable.run()方法
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
每种日志组件的加载都是调用setImplementation方法,这里以Slf4j为例,如下:
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
/**
* 根据指定适配器实现类加载相应的日志组件
*/
private static void setImplementation(Class<? extends Log> implClass) {
try {
//获取指定适配器的构造方法
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
//实例化适配器
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}