背景
本文作为《日志框架》专题的第二篇, 介绍Mybatis关于日志的实现,会涉及源码介绍。
Mybatis没有重复造轮子, 通过接入第三方日志组件实现日志功能, 且通过适配器使得日志实现随着接入的日志框架变化而变化;另外, 通过动态代理实现JDBC和Mybatis日志的合一, 方便用户使用Mybatis的日志。
1.Log接口和实现类
Mybatis中定义了自己的Log接口和其实现类,先看一下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);
}
如上所示,提供了trace、debug、warn、error四种记录日志的级别,同时提供了isTraceEnabled
和 isDebugEnabled
两个用于判断当前日志级别的接口。
日志框架-1 jul与log4j与logback介绍中介绍的日志实现,Log在Mybatis的logging包中都有对应的实现类:
其中:StdOutImpl表示使用JDK的System.out和System.err进行日志输出,NoLoggingImpl表示不进行日志输出;其他Log实现类均通过适配模式,调用类名关联的日志框架从而实现日志功能, 这里以为JakartaCommonsLoggingImpl和Slf4jLoggerImpl为例进行介绍。
JakartaCommonsLoggingImpl源码如下:
package org.apache.ibatis.logging.commons;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
public class JakartaCommonsLoggingImpl implements org.apache.ibatis.logging.Log {
private final Log log;
public JakartaCommonsLoggingImpl(String clazz) {
log = LogFactory.getLog(clazz);
}
// 将所有方法委托给Log对象
@Override public boolean isDebugEnabled() {return log.isDebugEnabled();}
@Override public boolean isTraceEnabled() {return log.isTraceEnabled();}
@Override public void error(String s, Throwable e) {log.error(s, e);}
@Override public void error(String s) {log.error(s);}
@Override public void debug(String s) {log.debug(s);}
@Override public void trace(String s) {log.trace(s);}
@Override public void warn(String s) {log.warn(s);}
}
JakartaCommonsLoggingImpl类结构比较简单,内部维持一个Log类对象,并将所有方法委托给这个对象实现;注意这里的Log是org.apache.commons.logging.Log
类型,且由org.apache.commons.logging.LogFactory
日志工厂生成。
Slf4jLoggerImpl源码如下:
package org.apache.ibatis.logging.slf4j;
import org.apache.ibatis.logging.Log;
import org.slf4j.Logger;
class Slf4jLoggerImpl implements Log {
private final Logger log;
public Slf4jLoggerImpl(Logger logger) {
log = logger;
}
// 将所有方法委托给Log对象
@Override public boolean isDebugEnabled() {return log.isDebugEnabled();}
@Override public boolean isTraceEnabled() {return log.isTraceEnabled();}
@Override public void error(String s, Throwable e) {log.error(s, e);}
@Override public void error(String s) {log.error(s);}
@Override public void debug(String s) {log.debug(s);}
@Override public void trace(String s) {log.trace(s);}
@Override public void warn(String s) {log.warn(s);}
}
可以看出Slf4jLoggerImpl关于日志接口的实现也是通过内部维持一个org.slf4j.Logger
类对象,并将所有方法委托给改类实现。其他实现类除去再次封装和适配,实现原理基本相似。
2.LogFactory
如章节1中所述,Mybatis为Log接口提供了11种实现类,而没有介绍程序运行时具体使用哪种日志框架;LogFactory作为Mybatis的日志工厂,提供了一种选择策略:基于日志框架是否被引入。
在分析LogFactory功能前,先看一下Mybatis的pom.xml文件:
Mybatis的pom.xml文件中日志片段依赖:
由于JakartaCommonsLoggingImpl等Log实现类中引用了jcl, log4j以及slf4j等日志对象,因此Mybatis-x.x.x.jar
包必须依赖日志框架,如上所示。
另外,mybatis对日志框架依赖项中的标签均为true, 表示依赖不会进行传递;即依赖mybatis的项目不会因此间接依赖日志框架。
分析LogFactory源码前,有必要先交代一下,Mybatis打印日志前LogFactory因没有使用而不会被加载;第一次打印日志时才会被加载进JVM, 加载时会先执行static代码块。
类加载相关技术可以参考JVM-3 类加载机制(上)和 JVM-3 类加载机制(下)
LogFactory源码:
// 省去部分无关逻辑, 调整代码顺序,请忽略代码规范
public final class LogFactory {
// =========================第一部分:日志工厂API=========================
private static Constructor<? extends Log> logConstructor;
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);
}
}
// =========================第二部分:静态代码初始化过程=========================
static {
// 框架为兼容jdk版本使用匿名类形式,这里为了阅读方便改成lambda表达式
tryImplementation(() -> useSlf4jLogging());
tryImplementation(() -> useCommonsLogging());
tryImplementation(() -> useLog4J2Logging());
tryImplementation(() -> useLog4JLogging());
tryImplementation(() -> useJdkLogging());
tryImplementation(() -> useNoLogging());
}
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 useCommonsLogging() {setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);}
public static synchronized void useLog4JLogging() {setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);}
public static synchronized void useLog4J2Logging() {setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);}
public static synchronized void useJdkLogging() {setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);}
public static synchronized void useStdOutLogging() {setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.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 = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
}
LogFactory类按照功能可以被划分为两个部分:第一部分为本职工作,作为工厂以静态方法getLog对外提供获取日志对象的能力,内部通过反射调用logConstructor.newInstance(logger)
创建日志对象;logConstructor对象的初始化工作在第二部分代码中实现。
第二部分代码执行发生在LogFactory类加载阶段,入口为static代码块:
static {
// 框架为兼容jdk版本使用匿名类形式,这里为了阅读方便改成lambda表达式
tryImplementation(() -> useSlf4jLogging());
tryImplementation(() -> useCommonsLogging());
tryImplementation(() -> useLog4J2Logging());
tryImplementation(() -> useLog4JLogging());
tryImplementation(() -> useJdkLogging());
tryImplementation(() -> useNoLogging());
}
tryImplementation(() -> useXxxLogging());
只是对useXxxLogging()
进行了一层包装:捕获useXxxLogging()
方法抛出的异常并直接忽略; 且执行前会进行logConstructor属性的非空判断,为空才执行useXxxLogging()
; 由此确定了优先级:Slf4j > CommonsLog > Log4J2 > Log4J > JdkLog > NoLog
useXxxLogging()
方法内部逻辑相同,以最高优先级的useSlf4jLogging()
为例进行介绍:
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
此时,如果项目没有引入Slf4j框架,会在加载org.apache.ibatis.logging.slf4j.Slf4jImpl
类时(因加载其引用的属性类型org.slf4j.Logger
失败)抛出NotClassDefFoundError
异常, 异常被tryImplementation方法捕获并忽略,然后继续向下调用useCommonsLogging()
尝试加载…直到找到一个可被使用的日志框架;其中,Mybatis提供的useNoLogging日志实现类作为保底方案。
如果此时项目引入了Slf4j依赖,则org.apache.ibatis.logging.slf4j.Slf4jImpl
正常加载,然后调用setImplementation(Class<? extends Log> implClass)
方法将Slf4jImpl的带String构造函数对象赋值给logConstructor属性,完成初始化工作。
另外,也可以直接在Mybatis的配置文件中通过指定日志实现类。
3.整合Mybatis日志与JDBC日志
Mybatis作为一个ORM框架,作用是进行数据库信息(入参、返回值)与Java对象的映射,并不直接参与数据库操作。数据库的读写操作由JDBC完成,且JDBC有一套自己的日志输入系统;因此无法通过Mybatis框架统一打印出映射过程的日志和数据库操作的日志。
这给用户定位问题带来了不便,因为Mybatis中的异常经常是由于数据库操作导致的,如查询异常导致的返回结果无法映射成Java对象;查询失败是参数的问题、SQL的问题还是返回结果的问题,这些通过JDBC的数据库操作日志可以快速得出结论。
Mybatis在logging包中增加了一个jdbc子包,通过代理解决上述问题:
包中仅有5个类,抽出公共部分形成BaseJdbclogger作为基类;ConnectionLogger
,PreparedStatementLogger
,ResultSetLogger
,StatementLogger
四个类对应不同的数据库操作场景且均实现了InvocationHandler接口,通过动态代理实现JDBC和Mybatis的日志合一,以下以ConnectionLogger
为例进行介绍。
Mybatis获取connection对象涉及的源码:
protected Connection getConnection(Log statementLog) throws SQLException {
Connection connection = transaction.getConnection();
if (statementLog.isDebugEnabled()) {
return ConnectionLogger.newInstance(connection, statementLog, queryStack);
} else {
return connection;
}
}
通过事务对象获取Connection对象后,根据是否启用调试日志走不同的逻辑分支:未开启时,直接返回原Connection对象;否则通过ConnectionLogger.newInstance(connection, statementLog, queryStack)
返回包装后的代理类。
跟进ConnectionLogger的newInstance方法:
public static Connection newInstance(Connection conn, Log statementLog, int queryStack) {
InvocationHandler handler = new ConnectionLogger(conn, statementLog, queryStack);
ClassLoader cl = Connection.class.getClassLoader();
return (Connection) Proxy.newProxyInstance(cl, new Class[]{Connection.class}, handler);
}
因此,ConnectionLogger.newInstance
方法返回一个Connection类对象(动态代理对象),且该对象的方法被调用时–调用会转发到ConnectionLogger这个InvocationHandler实现类的invoke方法中;动态代理部分内容请参考:JAVASE-14 静态代理与动态代理.
跟进ConnectionLogger的invoke方法:
如上所示,在调用Connection对象的prepareStatement或者prepareCall之前,可以打印出传递给sql或者存储过程的参数;即在代理Connection过程中对其进行增强,添加了日志打印功能。