良好的日志在一个软件中占了非常重要的地位,日志是开发与运维管理之间的桥梁 。 在 Java 开发中常用的日志框架有 Log4j 、 Log4j2 、 Apache Commons Log 、 java.util.logging、slf.句等,这些工具对外的接口不尽相同。为了统一这些工具的接口, MyBatis 定义了 一套统一的日志接口供上层使用 , 并为上述常用的日志框架提供了相应的适配器。
我们在使用的时候需要添加一个日志的配置文件,否则Mybatis可能无法启动。日志配置好之后,我们在开发的时候除了添加打印,一般不会做什么,内部原理也不需要考虑,但是这个模块的实现整体是一个典型的适配器模式,我们就来研究一下适配器模式,以及如何在Mybatis的日志模块中落地的吧。
1.适配器模式
1.1 什么是适配器模式
适配器模式的主要目的是解决由于接口不能兼容而导致类无法使用 的问题,适配器模式会将需要适配的类转换成调用者能够使用的目标接口。
适配器模式中涉及的几个角色:
- 目标接口( Target):调用者能够直接使用的接口 。
- 需要适配的类( Adaptee ): 一般情况下, Adaptee 类中有真正的业务逻辑,但是其接口不能被调用者直接使用。
- 适配器( Adapter): Adapter 实现了 Target 接 口,并包装了一个 Adaptee 对象。 Adapter在实现 Target 接口中的方法时,会将调用委托给 Adaptee 对象的相关方法,由 Adaptee完成具体的业务。
使用适配器模式的好处就是复用现有组件。应用程序需要复用现有的类,但接口不能被该应用程序兼容,则无法直接使用。这种场景下就适合使用适配器模式实现接口的适配,从而完成组件的复用。很明显,适配器模式通过提供 Adapter 的方式完成接口适配,实现了程序复用Adaptee 的需求,避免了修改 Adaptee 实现接口,这符合“开放-封 闭”原则 。当有新 的 Adaptee需要被复用时,只要添加新的 Adapter 即可,这也是符合“开放一封闭”原则的。
适配器模式与门面模式容易搞混,从功能的角度,两者很好区分。适配器模式是别人给你提供了接口,你发现不能直接用就给添加一个adapter类转换一下再用,而门面模式是你根据自己的理解给别人提供服务接口。
1.2 适配器的例子
创建步骤:
1.建立被适配的对象
public class Adptee {
public void request(){
System.out.println("可以完成用户的功能");
}
}
2.建立抽象接口,这定义了外部需要一个怎么样的功能。
public interface Target {
void handlerReq();
}
3.采用继承的方式建立接口的实现类
这种方式下,新类可以通过super获得父类的所有方法,因此可以根据新类的要求将父类的功能拆分后组合进来。也就是适配功能。
public class Adapter extends Adptee implements Target {
@Override
public void handlerReq() {
super.request();
}
}
4.上述方式的问题是强依赖父类,不够灵活,可以采用依赖的方式
public class Adapter2 implements Target {
private Adptee adptee;
@Override
public void handlerReq() {
adptee.request();
}
public Adapter2(Adptee adptee) {
this.adptee = adptee;
}
}
2.适配器模式在日志模块中的应用
在 MyBatis 的日志模块中,就使用了适配器模式 。 MyBatis 内 部调用其日志模块时,使用了其内部接口(也就是后面要介绍的 org.apache. atis.logging.Log 接口)。但是 Log4j 、 Log4j2等第三方日志组件对外提供的接口各不相同, MyBatis 为了集成和复用这些第三方日志组件,在其日志模块中提供了多种 Adapter ,将这些第 三方日志 组件对外的接口适配成了org.apach巳. atis.logging.Log 接口,这样 MyBatis 内部就可以统一通过 org.apache.ibati s.logging.Log 接口调用第三方日志组件的功能了。
当程序中存在过多的适配器时,会让程序显得非常复杂(后续介绍的所有模式都会有该问题,但是与其带来的好处进行权衡后,这个问题可以忽略不计),增加了把握住核心业务逻辑的难度,例如,程序调用了接口 A,却在又被适配成了接口 B 。 如果程序中需要大量的适配器 ,则不再优先考虑使用适配器模式,而是考虑将系统进行重构,这就需要设计人员进行权衡 。
前面描述的多种第三方日志组件都有各自的 Log 级别,且都有所不同,例如 java. util.logging提供了 All 、 FINEST 、 FINER、 FINE 、 CONFIG、卧.WO 、 W成NING 等 9 种级别,而 Log句2 则只有 trace 、 debug 、 info 、 warn 、 eηor 、 fatal 这 6 种日志级别。 MyBatis 统一提供了 trace 、 debug 、warn 、 eηor 四个级别,这基本与主流日志框架的日志级别类似,可以满足绝大多数场景的日志需求 。
MyBatis 的日志模块位于 org.apache.ibatis.logging 包中,该模块中通过 Log 接口定义了日志模块的功能,当然日志适配器也会实现此接口。 LogFactory 工厂类负责创建对应的日志组件适
配器,
Log接口的定义为:
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);
}
实现该接口的类有:
在 LogFactory 类加载时会执行其静态代码块,其逻辑是按序加载并实例化对应日志组件的
适配器,然后使用 LogFactory. logConstructor 这个静态宇段,记录当前使用的第三方日志组件的
适配器,具体代码如下所示。
public final class LogFactory {
public static final String MARKER = "MYBATIS";
// 记录当前使用的第二方日志组件所对应的适配器的构造方法
private static Constructor<? extends Log> logConstructor;
static {
下面会针对每种 日志组件调用 tryimplementation ( )方法进行尝试加载
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
private LogFactory() {
// disable construction
}
public static Log getLog(Class<?> aClass) {
return getLog(aClass.getName());
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
throw new LogException("Error creating logger for logger " + logger + ". Cause: " + t, t);
}
}
public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
setImplementation(clazz);
}
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
。。。。
public static synchronized void useNoLogging() {
setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
}
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
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 字段
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
其他各种日志模块例如Jdkl 4Logginglmpl 实现了Log 接口,并封装了 java.util.logging.
Logger 对象 , .Log 接口的功能全部通过调用 java.util.logging.Logger 对象
实现, 这与前面介绍 的适配器模式完全一致。
从上面我们也可以看到适配器和简单工厂模式搭配来构造了日志服务的框架。