1. 概述:
在 Java 开发中常用的日志框架有 Log4j 、 Log4j2 、 Apache Commons Log 、java.util.logging.slfj等,这些工具对外的接口不尽相同。为了统一这些工具的接口, MyBatis 定义了 一套统一的日志接口供上层使用 ;在MyBatis的日志模块中,用到了常用的设计模式适配器模式,代理模式。
MyBatis 的日志模块位于 org.apache.ibatis.logging 包中,如下图所示,为日志模块所在的包;在该模块,通过 Log 接口定义了日志模块的功能。在本篇博文中会结合两种设计模式进行分析。
2 开源日志框架适配:
2.1 Log接口
MyBatis 统一提供了 trace 、 debug 、warn 、 error 四个级别,这基本与主流日志框架的日志级别类似,可以满足绝大多数场景的日志需求;如下,为org.apache.ibatis.logging.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);
}
该接口定义了支持的集中默认的日志级别,比较简单,就不再阐述。
2.2 LogFactory
Log 工厂类,顾名思义,就说用来获取Log接口的。如下,为对应的静态初始化代码
public static final String MARKER = "MYBATIS";
private static Constructor<? extends Log> logConstructor;
static {
tryImplementation(LogFactory::useSlf4jLogging);
tryImplementation(LogFactory::useCommonsLogging);
tryImplementation(LogFactory::useLog4J2Logging);
tryImplementation(LogFactory::useLog4JLogging);
tryImplementation(LogFactory::useJdkLogging);
tryImplementation(LogFactory::useNoLogging);
}
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 = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
&n bsp; LogFactory.trylmplementation()方法首先会检测 logConstructor 字段,若为空则调用Runnable.run ()方法,注意不是start方法。如果对应的类能创建成功,意味着可以使用,设置为 logConstructor 。也就是说,其它类,就不会执行到该方法啦。
通过LogFactory来获取不同的日志框架,本质是要事先对成员logConstructor事先设置好具体的值,具体可参考getLog()方法,代码比较简单,就不再粘贴出来。
2.3 适配器模式
适配器模式的主要目的是解决由于接口不能兼容而导致类无法使用的问题。适配器模式会将需要适配的类转换成调用者能够使用的目标接口;适配器模式的类图如下所示:
- Target:目标接口:调用者能够直接使用的接口 ;在MyBatis日志模块中,为log接口
- Adaptee :一般情况下, Adaptee 类中有真正的业务逻辑,但是其接口不能被调用者直接使用,在MyBatis日志模块中,为各个不同的日志框架。
- Adapter: 实现了 Target 接 口,并包装了一个 Adaptee 对象。 Adapter在实现 Target 接口中的方法时,会将调用委托给 Adaptee 对象的相关方法,由 Adaptee完成具体的业务。在MyBatis模块中,如org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl,实现了log(Target)接口,包装了java.util.logging.Logger(adaptee)。
使用适配器模式的好处就是复用现有组件。应用程序需要复用现有的类,但接口不能被该应用程序兼容,则无法直接使用。这种场景下就适合使用适配器模式实现接口的适配,从而完成组件的复用。
当程序中存在过多的适配器时,会让程序显得非常复杂,增加了把握住核心业务逻辑的难度,例如,程序调用了接口 A,却在又被适配成了接口 B。
2.3.1测试用例
适配器模式的测试例子主要是博主没有再重新写了,直接复用了MyBatis项目对LogFactory的测试例(其实任何一个优秀的开源组件,其测试用例肯定覆盖的特别全面),项目代码在org.apache.ibatis.log-ging.LogFactoryTest测试类里面,
日志使用jdk自带的打印日志
@Test
void shouldUseJdKLogging() {
LogFactory.useJdkLogging();
Log log = LogFactory.getLog(Object.class);
logSomething(log);
assertEquals(log.getClass().getName(), Jdk14LoggingImpl.class.getName());
}
private void logSomething(Log log) {
log.warn("Warning message.");
log.debug("Debug message.");
log.error("Error message.");
log.error("Error with Exception.", new Exception("Test exception."));
}
测试结果:
日志使用Log4J打印日志
@Test
void shouldUseLog4J() {
LogFactory.useLog4JLogging();
Log log = LogFactory.getLog(Object.class);
logSomething(log);
assertEquals(log.getClass().getName(), Log4jImpl.class.getName());
}
private void logSomething(Log log) {
log.warn("Warning message.");
log.debug("Debug message.");
log.error("Error message.");
log.error("Error with Exception.", new Exception("Test exception."));
}
通过运行结果可以看出,通过适配器模式,完美解决了不同的日志框架适配的问题。
3.JDBC调试
3.1JDK动态代理
代理模式图谱关系如下图所示,该模式主要涉及到三个模型,分别为subject,realSubject,proxy,其中subject为业务接口,RealSubject为业务接口的实现,Proxy实现了Subject类,封装了RealSubject,在代理模式中,应用程序只会直接调用代理对象
3.2 JDK动态代理
JDK动态代理的核心在InvocationHandler接口,然后通过proxy#newProxyInstance()创建代理类;在调用该方法的时候,需要将InvocationHandler对象作为参数传入。在调用代理类的时候,实质是调用Invocation-Handler实现类的Invoke方法。JDK动态代理的本质是省略了我们手写代理类,通过我们在调用proxy#new-ProxyInstance()时传入的类加载器去帮我们自动生成代理类。代码如下:
Subject:
public interface Subject {
void action();
}
RealSubject
@Slf4j
public class RealSubject implements Subject {
@Override
public void action() {
log.info("realSubject do action.");
}
}
ProxySubject
@Slf4j
public class ProxySubject implements InvocationHandler {
private Subject target;
public ProxySubject(Subject target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
log.info("proxy前置执行.");
Object invoke = method.invoke(target, args);
log.info("proxy后置执行.");
return invoke;
}
public Object getProxy(){
return Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),target.getClass().getInterfaces(),
this);
}
}
测试用例
public class ProxyTest {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
ProxySubject proxySubject = new ProxySubject(realSubject);
Subject proxy = (Subject)proxySubject.getProxy();
proxy.action();
}
}
JDK 动态代理不仅在 MyBatis 的多个模块中都有所涉及 , 在很多开源框架中也能看到其身影,像spring,dubbo等都有JDK动态代理的身影。
3.3 jdbc调试
在 MyBatis 的日志模块中有一个 Jdbc 包,它并不是将日志信息通过 JDBC 保存到数据库中,而是通过 JDK 动态代理的方式,将 JDBC 操作通过指定的日志框架打印出来。这个功能通常在开发阶段使用 ,它可以输出 SQL 语句、用户传入的绑定参数、 SQL 语句影响行数等等信息,对调试程序来说是非常重要的。但本篇只展示具体的用法而不分析其原理,因为博主认为,该原理也涉及到参数结果的处理和MyBatis的执行流程,后续会有文章进行原理的分析。
如下所示,如果想打印真正执行SQL语句的。可以在MyBatis的配文件进行如下配置,既可以打印对应的sql语句
其实博主配置的日志类是MyBatis日志包下的StdOutImpl,感兴趣的读者也配置其它日志框架试试。
4总结
本篇主要是介绍了MyBatis的基础层日志模块,该模块其实代码不难,主要是涉及到了两种设计模式,适配器模式和JDK动态代理模式。如果讲解有啥错误之处,欢迎各位大佬留下评论探讨。