MyBatis3源码深度解析(十九)MyBatis日志实现

前言

日志是Java应用中必不可少的部分,它能够记录系统运行状况,有助于准确定位系统异常,不同的项目可能会使用不同的日志框架。

在整合了MyBatis的项目中,经常可以在日志文件中看到打印出来的SQL语句,那本节就来研究一下MyBatis的日志实现。

第七章 MyBatis日志实现

7.1 Java日志体系

7.1.1 常用日志框架

目前比较常用的日志框架有:

  • Log4j:Apache项目,是基于Java的日志记录工具。
  • Log4j 2:Log4j的升级产品。
  • Commons Logging:Apache项目,是一套Java日志接口。
  • SLF4J:也是一套Java日志接口。
  • Logback:SLF4J日志接口的实现。
  • JUL:JDK1.4之后提供的日志实现。

在实际项目中,通常会依赖很多第三方工具包或者框架,如果这些工具包或框架使用不同的日志实现,那么项目就要为每种不同的日志框架维护一套单独的配置,这会造成项目日志输出模块相当混乱。

然而,在实际项目中,又通常只维护一套日志配置。这个冲突是如何解决的?可以从Java日志发展史中得到答案。

7.1.2 Java日志发展史

  1. 1996年,Log4j问世,成为Apache基金会项目中的一员,近乎成为Java社区的日志标准;
  2. 2002年,JDK1.4发布,内置JUL(Java Util Logging)日志实现。
  3. 2002年,Apache推出JCL(Jakarta Commons Logging),定义了一套日志接口。
  4. 2006年,Log4j的作者离开Apache,先后创立了SLF4J(Simple logging Facade for Java,是一套日志接口)和Logback(SLF4J日志接口的实现)两个项目。
  5. 2012年,Apache为避免被Logback反超,重写了Log4j,成立了新的项目Log4j2。Log4j2具有Logback的所有特性。

总结一下,现如今Java日志划分为两大阵营:JCL阵营和SLF4J阵营。

JCL和SLF4J属于日志接口,提供统一的日志操作规范,输入日志功能由具体的日志实现框架(例如Log4j、Logback等)完成。 如图:

基于这样的关系,所有第三方工具包或者框架只需要确定自身符合日志接口定义的规范,就可以适用任何一种日志实现,这样就解决了日志实现冲突的问题。

7.1.3 日志接口与日志实现的绑定

日志接口需要与具体的日志实现框架进行绑定。

例如,项目使用JCL作为日志接口,则需要在classpath下新增一个commons-logging.properties文件,通过该文件指定日志框架的具体实现。例如:

# commons-logging.properties
org.apache.commons.logging.Log=org.apache.commons.logging.impl.Jdk14Logger

如果需要修改具体的日志实现,则只需要修改org.apache.commons.logging.Log属性值,应用代码无序做任何调整。

SLF4J框架中定义了日志接口,各个日志实现框架只需要遵循这个接口,就能够做到日志系统间的无缝兼容。适用SLF4J接口的实现框架又有两种模式:桥接模式和适配器模式。

  • 使用桥接模式的日志实现框架有:jcl-over-SLF4J(把对JCL的调用桥接到SLF4J)、jul-to-SLF4J(把对JUL的调用桥接到SLF4J)、log4j-over-SLF4J(把对Log4j的调用桥接到SLF4J)。

  • 使用适配器模式的日志实现框架有:Logback(推荐使用,性能比Log4j好,且支持变参占位符日志输出方式)、SLF4J-logj12(对Log4j的适配器)、SLF4J-jdk14(对JUL的适配器)。

在应用程序中,如果使用SLF4J接口编写日志输出代码,除了引入SLF4J-api.jar依赖,还需要根据底层日志框架不同,同时引入对应的依赖:

  • 底层使用Log4j:slf4j-log412.jar、log4j.jar
  • 底层使用Logback:logback-classic.jar、logback-core.jar
  • 底层使用JUL:slf4f-jdk14.jar、

7.2 MyBatis日志实现

7.2.1 Log接口

MyBatis通过Log接口定义日志操作规范,其定义如下:

源码1org.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);
}

MyBatis针对不同的日志框架提供对Log接口对应的实现,如图所示:

其中包括JCL、JUL、Log4j2、Log4j、No Logging(不输出任何日志)、SLF4J、Stdout(将日志输出到标准输出设备,如控制台)。

7.2.2 LogFactory工厂

MyBatis的Log实例采用工厂模式创建,即LogFactory类,该类提供了一系列useXXXLogging()方法,用于指定具体使用哪种日志实现类输出日志。

源码2org.apache.ibatis.logging.LogFactory

public final class LogFactory {

    // ......
    
    // 自定义日志实现
    public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
        setImplementation(clazz);
    }
    
    // 使用SLF4J框架输出日志
    public static synchronized void useSlf4jLogging() {
        setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
    }
    
    // 使用JCL框架输出日志
    public static synchronized void useCommonsLogging() {
        setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
    }
    
    // 使用Log4j框架输出日志
    @Deprecated
    public static synchronized void useLog4JLogging() {
        setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
    }
    
    // 使用Log4j2框架输出日志
    public static synchronized void useLog4J2Logging() {
        setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
    }
    
    // 使用JUL框架输出日志
    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);
    }
}

由 源码2 可知,每一个useXXXLogging()方法都会调用setImplementation()方法,指定日志实现类。

源码3org.apache.ibatis.logging.LogFactory

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);
    }
}

由 源码3 可知,setImplementation()方法首先获取日志实现类对应的Constructor对象,然后根据该对象创建一个Log实例,并将该对象保存在logConstructor属性中。

接下来以Slf4jImpl实现类为例,研究一下MyBatis的日志实现:

源码4org.apache.ibatis.logging.slf4j.Slf4jImpl

public class Slf4jImpl implements Log {

    private Log log;
    
    public Slf4jImpl(String clazz) {
        Logger logger = LoggerFactory.getLogger(clazz);
        // ......
        log = new Slf4jLoggerImpl(logger);
    }
    
    // isDebugEnabled ...
    // isTraceEnabled ...
    // error ...
    // debug ...
    // trace ...
    // warn ...
}

由 源码4 可知,在Slf4jImpl的构造方法中,通过LoggerFactory获取SLF4J框架中的Logger对象,然后创建了一个Slf4jLoggerImpl实例。

源码5org.apache.ibatis.logging.slf4j.Slf4jLoggerImpl

class Slf4jLoggerImpl implements Log {
    private final Logger log;
    
    public Slf4jLoggerImpl(Logger logger) {
        log = logger;
    }

    // isDebugEnabled ...
    // isTraceEnabled ...
    // error ...
    // debug ...
    // trace ...
    // warn ...
}

由 源码5 可知,在Slf4jLoggerImpl的构造方法中,将日志输出相关操作委托给SLF4J框架中的Logger对象来完成。

因此,在调用LogFactory的useSlf4jLogging()方法时,就确定了使用org.apache.ibatis.logging.slf4j.Slf4jImpl实现类输出日志,而Slf4jImpl实现类又将日志输出操作委托给SLF4J框架的Logger对象,这样就确定了使用SLF4J框架输出日志。

下面是使用SLF4J日志框架的案例:

@Test
public void testLog() {
    // 指定使用SLF4J框架输出日志
    LogFactory.useSlf4jLogging();
    // 获取Log实例并输出日志
    Log log = LogFactory.getLog(Slf4jImpl.class);
    log.debug("test Slf4jImpl");
}

控制台打印执行结果:

SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.

打印这样的结果是因为,SLF4J本身只是一个日志门面,在没有具体的日志实现时,默认使用NOP(no-operation)实现,即不打印任何日志。

7.2.3 MyBatis日志自动查找

MyBatis日志模块设计地比较巧妙的是,当未指定使用哪种日志实现时,MyBatis将按照顺序查找classpath下的日志框架相关的jar包。如果classpath下有对应的日志包,则使用该日志框架打印日志。

源码6org.apache.ibatis.logging.LogFactory

public final class LogFactory {
    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) {
        // 先判断logConstructor属性是否为空
        // 如果为空,则说明还没有指定日志实现框架,继续往下查找
        // 如果不为空,则说明已经指定了日志实现框架,不再继续往下查找
        if (logConstructor == null) {
            try {
                runnable.run();
            } catch (Throwable t) {
                // ignore
            }
        }
    }
}

由 源码6 可知,在LogFactory类中有一个初始代码块,按照一定的顺序调用tryImplementation()方法,以确定日志实现类,该方法的参数是一个Runnable匿名对象,在run()方法中调用LogFactory中的静态useXXXLogging()方法。

需要注意的是,这里虽然使用了Runnable接口,但跟多线程无关,仅仅是把run()方法作为一个普通方法调用。 因此,在该静态代码块中,首先通过tryImplementation()方法尝试调用LogFactory的useSlf4jLogging()方法使用SLF4J日志框架。而在useSlf4jLogging()方法中,会获取SLF4J日志框架的Logging对象。

如果classpath中存在SLF4J日志框架的依赖,则会将LogFactory的logConstructor属性指定为org.apache.ibatis.logging.slf4j.Slf4jImpl类对应的Constructor对象。而tryImplementation()方法中首先会判断logConstructor属性是否为空,因此后续设置日志实现类的逻辑不会再执行。

如果classpath中不存在SLF4J日志框架的依赖,则useSlf4jLogging()方法会抛出ClassNotFoundException和NoClassDefFoundException异常(它们都实现了Throwable接口)。由 源码6 可知,tryImplementation()方法会捕获这两个异常,但不做任何处理,仅仅只是捕获而已。

紧接着,调用tryImplementation(LogFactory::useCommonsLogging);查找classpath下是否有JCL日志框架的相关依赖。

总结一下,MyBatis查找日志框架的顺序为:SLF4J→JCL→Log4j2→Log4j→JUL→No Logging。如果classpath下不存在任何日志框架的依赖,则使用NoLoggingImpl日志实现类,即不输出任何日志。

7.2.4 MyBatis日志类型配置

在使用MyBatis时,还可以通过MyBatis主配置文件中的<setting name="logImpl" value="SLF4J"/>参数指定使用哪种日志框架。

源码7org.apache.ibatis.builder.xml.XMLConfigBuilder

private void loadCustomLogImpl(Properties props) {
    // 读取配置文件中的logImpl参数
    // 并将其转换为对应的Class对象
    Class<? extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
    // 注册到Configuration对象中
    configuration.setLogImpl(logImpl);
}
源码8org.apache.ibatis.builder.BaseBuilder

protected <T> Class<? extends T> resolveClass(String alias) {
    try {
        return alias == null ? null : resolveAlias(alias);
    } // catch ...
}
protected <T> Class<? extends T> resolveAlias(String alias) {
    // 从别名注册器中获取Class对象
    // 说明logImpl参数配置的是一个别名
    return typeAliasRegistry.resolveAlias(alias);
}
源码9org.apache.ibatis.session.Configuration

public class Configuration {
    // ......
    protected Class<? extends Log> logImpl;
    public 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);
        // ......
    }
    
    public Class<? extends Log> getLogImpl() {
        return logImpl;
    }
    
    public void setLogImpl(Class<? extends Log> logImpl) {
        if (logImpl != null) {
            this.logImpl = logImpl;
            // 设置日志实现类
            LogFactory.useCustomLogging(this.logImpl);
        }
    }
}

由 源码7-9 可知,当MyBatis框架启动时,会解析主配置文件的logImpl参数,并通过别名注册器TypeAliasRegistry将参数值转换为对应的Class对象,再调用Configuration对象的setLogImpl()将日志实现类的Class对象保存在logImpl属性中,并调用LogFactory的useCustomLogging()方法设置日志实现类。

由此可见,logImpl参数配置的是日志实现类的别名,这些别名的定义在Configuration对象的构造方法中完成,该参数的可选值有:SLF4J、COMMONS_LOGGING、LOG4J、LOG4J2、JDK_LOGGING、STDOUT_LOGGING、NO_LOGGING。

7.3 小结

第七章到此就梳理完毕了,本章的主题是:MyBatis日志实现。回顾一下本章的梳理的内容:

(十九)Java日志体系、MyBatis日志实现

更多内容请查阅分类专栏:MyBatis3源码深度解析

第八章主要学习:动态SQL实现原理。主要内容包括:

  • 动态SQL的使用;
  • SqlSource与BoundSql原理;
  • LanguageDriver原理;
  • SqlNode原理;
  • 动态SQL解析过程;
  • #{}和${}的区别。
  • 26
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值