MyBatis日志实现二-MyBatis日志实现
本节我们来了解一下MyBatis的日志实现。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);
}
MyBatis针对不同的日志框架提供对Log接口对应的实现,Log接口的实现类如图所示。从实现类可以看出,MyBatis支持7种不同的日志实现,具体如下。Apache Commons Logging:使用JCL输出日志。
Log4j 2:使用Log4j 2框架输入日志。
Java Util Logging:使用JDK内置的日志模块输出日志。
Log4j:使用Log4j框架输出日志。
No Logging:不输出任何日志。
SLF4J:使用SLF4J日志门面输出日志。
Stdout:将日志输出到标准输出设备(例如控制台)。
Log实现类的逻辑比较简单,只是调用对应日志框架相关的API打印日志。以Log4jImpl实现类为例,代码如下:
package org.apache.ibatis.logging.log4j;
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
/**
* @author Eduardo Macarron
*/
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
private final Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
@Override
public boolean isDebugEnabled() {
return log.isDebugEnabled();
}
@Override
public boolean isTraceEnabled() {
return log.isTraceEnabled();
}
@Override
public void error(String s, Throwable e) {
log.log(FQCN, Level.ERROR, s, e);
}
@Override
public void error(String s) {
log.log(FQCN, Level.ERROR, s, null);
}
@Override
public void debug(String s) {
log.log(FQCN, Level.DEBUG, s, null);
}
@Override
public void trace(String s) {
log.log(FQCN, Level.TRACE, s, null);
}
@Override
public void warn(String s) {
log.log(FQCN, Level.WARN, s, null);
}
}
如上面的代码所示,在Log4jImpl构造方法中,获取Log4j框架中的Logger对象,然后将日志输出操作委托给Logger对象来完成。其他日志实现类逻辑与之类似,具体可参考MyBatis相关源码。
MyBatis支持7种不同的日志输出策略,在实际使用MyBatis框架时,具体使用哪种方式输出日志呢?
接下来我们就来揭开这个谜团。MyBatis的Log实例采用工厂模式创建。下面是使用LogFactory获取Log实例的案例,代码如下:
public void testLog() {
// 指定使用Log4j 框架输出日志
LogFactory.useLog4JLogging();
// 获取Log实例
Log log = LogFactory.getLog(LogExample.class);
// 输出日志
log.error("LogExample.testLog function.");
}
在上面的代码中,首先调用LogFactory类的useLog4JLogging()方法显式地指定使用Log4j框架对应的Log实现类打印日志。除了useLog4JLogging()方法外,LogFactory中还提供了一系列类似的方法,用于指定具体使用哪种日志实现类输出日志,这些方法如下:
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);
}
这些方法中,都会调用setImplementation()方法指定日志实现类。我们可以了解一下LogFactory类setImplementation()方法的实现,内容如下:
private static void setImplementation(Class<? extends Log> implClass) {
try {
// 获取日志实现类的Constructor对象
Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
// 根据日志实现类创建Log实例
Log log = candidate.newInstance(LogFactory.class.getName());
if (log.isDebugEnabled()) {
log.debug("Logging initialized using '" + implClass + "' adapter.");
}
// 记录当前使用的日志实现类的Constructor对象
logConstructor = candidate;
} catch (Throwable t) {
throw new LogException("Error setting Log implementation. Cause: " + t, t);
}
}
如上面的代码所示,在setImplementation()方法中,首先获取MyBatis日志实现类对应的Constructor对象,然后通过LogFactory类的logConstructor属性记录当前日志实现类的Constructor对象。所以当我们调用LogFactory类的useLog4JLogging()方法时,就确定了使用
org.apache.ibatis.logging.log4j.Log4jImpl实现类输出日志,而Log4jImpl实现类又将日志输出操作委托给Log4j框架,这样就确定了使用Log4j框架输出日志。
MyBatis日志模块设计得比较巧妙的一点是当我们未指定使用哪种日志实现时,MyBatis能够按照顺序查找Classpath下的日志框架相关JAR包。如果Classpath下有对应的日志包,则使用该日志框架打印日志。
接下来我们了解一下MyBatis动态查找日志框架的实现细节。在LogFactory类中有一个初始化代码块,内容如下:
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()方法确定日志实现类,tryImplementation()方法的参数是一个Runnable的匿名对象,在run()方法中调用useSLF4JLogging()等方法指定日志实现类。提到Runnable接口,我们容易联想到Java的多线程,但是这里和线程没有任何关系。我们可以关注一下tryImplementation()方法的实现,代码如下:
private static void tryImplementation(Runnable runnable) {
if (logConstructor == null) {
try {
runnable.run();
} catch (Throwable t) {
// ignore
}
}
}
如上面的代码所示,在tryImplementation()方法中,只是将Runnable匿名对象的run()方法作为普通方法调用,所以这里不涉及任何Java多线程相关的内容。
回归正题,在LogFactory初始化代码块中,首先调用tryImplementation()尝试调用useSLF4JLogging()方法使用SLF4J日志框架,如果Classpath中不存在SLF4J日志框架相关的JAR包,则useSLF4JLogging()方法会抛出ClassNotFoundException异常或NoClassDefFoundError错误。ClassNotFoundException和NoClassDefFoundError都实现了Throwable接口,tryImplementation()方法中对ClassNotFoundException和NoClassDefFoundError都进行捕获,不做任何处理,然后查找下一个日志框架JAR包是否存在,直至找到Classpath中存在的日志框架。
若Classpath中存在SLF4J框架相关JAR包,则使用SLF4JImpl日志实现类输出日志,并将LogFacotry的logConstructor属性指定为SLF4JImpl类对应的Constructor对象,由于tryImplementation()方法中会判断logConstructor是否为空,因此后续设置日志实现类的逻辑都不会执行。MyBatis查找日志框架的顺序为SLF4J→JCL→Log4j 2→Log4j→JUL→No Logging。如果Classpath下不存在任何日志框架,则使用NoLoggingImpl日志实现类,即不输出任何日志。
在使用MyBatis时,我们还可以通过logImpl参数指定使用哪种框架输出日志,例如:
<settings>
<setting name="useGeneratedKeys" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
<setting name="logImpl" value="LOG4J"/>
<setting name="cacheEnabled" value="true"/>
</settings>
logImpl属性可能的属性值有SLF4J、COMMONS_LOGGING、LOG4J、LOG4J2、JDK_LOGGING、STDOUT_LOGGING、NO_LOGGING或日志实现类的完全限定名,原因是Configuration类的构造方法中,为这些日志实现类注册了别名,代码如下:
typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
另外,Configuration类中维护了一个logImpl属性,当MyBatis框架启动时,解析主配置文件中的logImpl参数,然后调用Configuration类的setLogImpl()方法设置日志实现类。setLogImpl()方法内容如下:
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl;
// 调用LogFactory类的useCustomLogging()方法指定日志实现类
LogFactory.useCustomLogging(this.logImpl);
}
}
如上面的代码所示,在Configuration类的setLogImpl()方法中,调用LogFactory类的useCustomLogging()方法指定日志实现类。而MyBatis中所有的Log实例都是由LogFactory创建的,这样就保证了整个系统输出日志使用同一种框架。