Mybatis日志实现
1. 首先看下源码包的结构:
可以大致看出Mybatis支持7种不同的日志实现。
说到Java日志不得不提SLF4J与apache的JCL属于日志门面,提供统一的日志操作规范,输入日志功能由具体的日志实现框架完成(log4j、log4j2、JUL、logback等)。
2. 源码阅读
Mybatis通过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);
}
针对不同的日志框架提供对Log接口的实现,如下图:
7种不同实现:
Apache Commons Logging:
Log4J:
Log4J2:
Java Util Logging:
No Logging:
SLF4J:
Stdout:
以Log4J为例,Log现实类逻辑如下:
import org.apache.ibatis.logging.Log;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
public class Log4jImpl implements Log {
private static final String FQCN = Log4jImpl.class.getName();
// Log4J接口类
private final Logger log;
public Log4jImpl(String clazz) {
log = Logger.getLogger(clazz);
}
...//省略
}
可以看出构造方法中,获取Log4J框架中的Logger对象,然后将日志输出操作委托给Logger类的对象log来完成。其他日志实现与之类似。
MyBatis框架如何使用日志呢?以org.apache.ibatis.logging.LogFactoryTest作参考:
@Test
void shouldUseLog4J() {
LogFactory.useLog4JLogging();
Log log = LogFactory.getLog(Object.class);
logSomething(log);
assertEquals(log.getClass().getName(), Log4jImpl.class.getName());
}
从代码片段看 LogFactory,很明显MyBatis框架Log采用工厂模式创建。我们先来看看LogFactory.useLog4JLogging();
public static synchronized void useLog4JLogging() {
setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
}
public final class LogFactory {
// 当前使用的日志实现类的Constructor
private static Constructor<? extends Log> logConstructor;
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框架。
我们发现LogFactory内容不多,把它读完。
public final class LogFactory {
/**
* Marker to be used by logging implementations that support markers.
*/
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
}
}
}
发现这个静态代码块比较特别,再接着看tryImplementation(),发现方法参数是一个Runnable的匿名对象,很容易联想Java的多线程,但是仔细一看这里知识把run()方法作为普通方法调用,所以不涉及到多线程。
MyBatis日志模块设计比较巧妙的地方就是我们没有指定使用哪种日志实现,MyBatis能够按照顺序查找Classpath下的日志框架相关Jar包。大胆推测加载顺序SLF4J->JCL->Log4J2->Log4J->JUL->No Logging。小心证实,如果Classpath中不存在Log4J,则setImplementation()方法中 Constructor<? extends Log> candidate = implClass.getConstructor(String.class); 会抛出ClassNotFoundException异常或者NoClassDefFoundError错误。进而在catch中普获重新封装并且抛出
throw new LogException("Error setting Log implementation. Cause: " + t, t);
在tryImplementation()方法中,进行进一步捕获,不做任何处理,然后查找下一个日志框架JAR包是否存在,直至找到为止。
3. MyBatis使用日志方式
在使用MyBatis时候,可以通过setting属性中的logImpl参数指定哪种框架输出日志。
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
logImpl属性限定值有SLF4J | LOG4J2 | LOG4J | JDK_LOGGING | COMMONS_LOGGING | STDOUT_LOGGING | NO_LOGGING
或者日志实现类的完全限定名,原因是Configuration类的构造方法中,为这些日志实现类注册了别名,代码如下:
public class Configuration {
//日志属性
protected Class<? extends Log> logImpl;
public Configuration() {
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
...//省略
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);
...//省略
}
public void setLogImpl(Class<? extends Log> logImpl) {
if (logImpl != null) {
this.logImpl = logImpl;
LogFactory.useCustomLogging(this.logImpl);
}
}
当框架启动时,解析主配置文件中的logImpl参数,然后调用setLogImpl()方法中设置日志实现。MyBatis中所有的Log实例都是由LogFactory创建的。