鲜为人知,为何不要使用MyBatis标准日志输出?

我们在使用MyBatis或衍生产品时,通常会打开其默认日志输出功能,通过SQL日志来排查问题。但你是否注意过为什么MyBatis日志总是与其它Log格格不入,没有日期、级别等等,全部靠左侧输出?

在指定默认日志输出时,经常使用一个名为StdOutImpl的类:

java 复制代码
public class StdOutImpl implements Log {
	...
    public void debug(String s) {
        System.out.println(s);
    }

    public void trace(String s) {
        System.out.println(s);
    }
    ...
}

默认使用System.out.println()输出日志,众所周知,其输出时会阻塞当前线程,导致SQL查询不得不等待日志输出完后才能返回结果。

这也是大多数文章推荐使用Log而不是System.out.println()的原因,因为Log是异步的。

但并发量不大的时候,可能感知不到System.out.println()带来的负面影响。

更糟糕的情况

接口响应缓慢、CPU占用率超高,系统几乎处于不可用状态,我曾怀疑SQL写的有问题,但在数据库中查询却又没有多少活跃线程。

直到使用jstack命令检查,发现多个web线程都在等待获取java.io.PrintStream的锁。

这就不得不提到System.out.println更糟糕的问题,如果进一步检查println()函数就会发现:

java 复制代码
public void println(String x) {
    synchronized (this) {
        print(x);
        newLine();
    }
}

System.out是一个全局静态PrintStream对象,而println()函数又会对其加锁调用,这就导致所有线程的SQL日志都是在排队输出。这在上一节问题的基础上,又导致了更严重的并发性能问题。

只需有几个像批量插入或数据导出这样有大量参数或返回结果的SQL语句,就可能影响所有接口的响应速度。

如何优化

内置Log实现

MyBatis中其实还提供了其它日志输出实现,例如使用Slf4jImpl代替StdOutImpl即可。

这不仅可以解决日志输出的性能问题,也可以通过Log库配置统一SQL日志的格式和输出位置。

MyBatis原生:

xml 复制代码
<configuration>
  <settings>
    <setting name="logImpl" value="SLF4J"/>
  </settings>
</configuration>

MyBatis Plus:

yaml 复制代码
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl

MyBatis Flex:

java 复制代码
@Configuration
public class MyBatisFlexConfig implements ConfigurationCustomizer {
    @Override
    public void customize(FlexConfiguration config) {
        // 使用Slf4j输出日志
        config.setLogImpl(Slf4jImpl.class);
    }
}

但注意的是,MyBatis Log接口中是没有info级别的:

java 复制代码
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);
}

SQL语句(Preparing)、SQL参数(Parameters)、结果数量(Total)是调用debug()方法打印,而结果列(Columns、Row)是调用trace()打印的。

因为Log通常只会打印info级别,还需要在Log配置中指定日志输出级别。例如logback的配置:

xml 复制代码
<logger name="org.example" level="TRACE" additivity="true" />

MyBatis会为每个Mapper中的每个方法创建一个Log对象,所以前缀一般只需取到项目的根目录即可,但如果还引入根包名不同的子模块,还得一并添加。

自定义Log实现

或者我们可以实现自己的Log,将输出改为info级别:

java 复制代码
public class MyBatisLogImpl implements Log {
    private final Logger log;

    public MyBatisLogImpl(String clazz) {
        log = LoggerFactory.getLogger(clazz);
        System.out.println(clazz);
    }

    @Override
    public boolean isDebugEnabled() {
        return true;
    }

    // 如果要打印具体查询结果(Columns & Row),修改返回值为true
    // 很多时候查询结果的日志是没必要的,关掉后可显著减少日志文件的体积
    @Override
    public boolean isTraceEnabled() {
        return true;
    }

    @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) {
        // 输出:SQL语句(Preparing)、SQL参数(Parameters)、结果数量(Total)
        log.info(s);
    }

    @Override
    public void trace(String s) {
        // 输出:结果列(Columns、Row)
        log.info(s);
    }

    @Override
    public void warn(String s) {
        log.warn(s);
    }
}

此外,MyBatis日志还有些没什么用的debug日志:

csharp 复制代码
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@729bcc15] was not registered for synchronization because synchronization is not active

也可以在日志配置中屏蔽:

xml 复制代码
 <!-- 排除SqlSessionUtils中的所有日志 -->
 <logger name="org.mybatis.spring.SqlSessionUtils" level="OFF" />

结论

System.out.println()是一种比预想中还要糟糕的日志输出方法,不止在MyBatis中不要使用StdOutImpl,在任何系统中都应该避免使用System.out.println()打印日志或依赖库基于其的实现,尤其是要警惕没有被Log库格式化过日志内容。

其问题远不止是在单线程中输出时会阻塞当前线程,在多线程环境中还会因为要对同一个System.out加锁使用,进而导致严重的并发性能问题。

相关推荐
飞的肖1 小时前
使用中间件自动化部署java应用
java·中间件·自动化
程序员沉梦听雨1 小时前
【IDEA】快捷键篇
java·ide·intellij-idea
Q_27437851093 小时前
django基于Python的智能停车管理系统
java·数据库·python·django
兩尛4 小时前
maven高级(day15)
java·开发语言·maven
xiao--xin5 小时前
LeetCode100之搜索二维矩阵(46)--Java
java·算法·leetcode·二分查找
end_SJ5 小时前
c语言 --- 字符串
java·c语言·算法
zzyh1234565 小时前
spring cloud 负载均衡策略
java·spring cloud·负载均衡
涔溪5 小时前
JS二叉树是什么?二叉树的特性
java·javascript·数据结构
zzyh1234565 小时前
springcloud负载均衡原理
java·spring cloud·负载均衡
东北赵四5 小时前
JVM之垃圾回收器G1概述的详细解析
java·开发语言·jvm