前言

在开发过程中,往往需要把执行的Sql给打印出来,通过查看SQL日志可以帮助开发者了解应用程序实际执行的SQL语句,这对于调试数据库相关的错误或查询问题非常有帮助。在此记录一下MyBatis开启SQL日志的相关配置。

官方文档链接

Spring+MyBatis项目

在项目中的mybatis-config.xml文件中增加如下配置:

<configuration>
  <settings>
    ...
    <setting name="logImpl" value="LOG4J"/>
    ...
  </settings>
</configuration>

Spring Boot+MyBatis项目

在项目中的Application.properties文件中添加如下配置:

mybatis.configuration.log-impl= org.apache.ibatis.logging.stdout.StdOutImpl

或在项目中的Application.yml文件中添加如下配置:

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

Spring Boot+MyBatisPlus项目

在项目中的Application.properties文件中添加如下配置:

mybatis-plus.configuration.log-impl= org.apache.ibatis.logging.stdout.StdOutImpl

或在项目中的Application.yml文件中添加如下配置:

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

控制台打印效果

此处使用一个查询地址的sql作为示例:

Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fe9a2fd] was not registered for synchronization because synchronization is not active
JDBC Connection [com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl@2f867381] will not be managed by Spring
==>  Preparing: SELECT id,name,province_id FROM city WHERE (province_id = ?) ORDER BY id ASC
==> Parameters: 44(Integer)
<==    Columns: id, name, province_id
<==        Row: 4401, 广州市, 44
<==        Row: 4402, 韶关市, 44
<==        Row: 4403, 深圳市, 44
<==        Row: 4404, 珠海市, 44
<==        Row: 4405, 汕头市, 44
<==        Row: 4406, 佛山市, 44
<==        Row: 4407, 江门市, 44
<==        Row: 4408, 湛江市, 44
<==        Row: 4409, 茂名市, 44
<==        Row: 4412, 肇庆市, 44
<==        Row: 4413, 惠州市, 44
<==        Row: 4414, 梅州市, 44
<==        Row: 4415, 汕尾市, 44
<==        Row: 4416, 河源市, 44
<==        Row: 4417, 阳江市, 44
<==        Row: 4418, 清远市, 44
<==        Row: 4419, 东莞市, 44
<==        Row: 4420, 中山市, 44
<==        Row: 4451, 潮州市, 44
<==        Row: 4452, 揭阳市, 44
<==        Row: 4453, 云浮市, 44
<==      Total: 21

此时,执行的SQL已经打印到控制台了。但是有的小伙伴就要说了:这个SQL里面占位符(?)参数还是分开的,复制到可视化工具还是不能直接执行好麻烦哦。

确实是的,相信很多小伙伴都遇到过这种情况,一条关联查询的SQL超长,参数也有很多个,这个时候如果SQL有错误,一时间还真看不出来问题到底出现在哪里。

自定义拦截器打印完整SQL

在MyBtis中,有个拦截器接口org.apache.ibatis.plugin.Interceptor,我们只需要实现此接口,就可以实现我们需要的功能。

实现后需要把拦截器给注册到对应的SqlSessionFactory中方可生效

@Intercepts({
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class,
                ResultHandler.class})
})
public class PrintSqlInterceptor implements Interceptor {

    private final static Logger log = LoggerFactory.getLogger(PrintSqlInterceptor.class);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取SQL映射信息对象
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        String sqlId = mappedStatement.getId();
        // 获取绑定的SQL对象,包含具体的SQL语句、参数等
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        // 获取配置对象
        Configuration configuration = mappedStatement.getConfiguration();
        long start = System.currentTimeMillis();
        Object returnValue = invocation.proceed();
        long time = System.currentTimeMillis() - start;
        String sql = this.parseCompleteSql(configuration, boundSql);
        log.info("{}SqlLog ==> statement={}, time={}(ms){}FullSql ==> {}", StringPool.NEWLINE, sqlId, time, StringPool.NEWLINE, sql);
        return returnValue;
    }

    public String parseCompleteSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        // 获取参数映射
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //替换空格、换行、tab缩进等
        String sql = boundSql.getSql().replaceAll("\\s+", " ");
        if (!parameterMappings.isEmpty() && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", getParameterValue(parameterObject));
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    if (metaObject.hasGetter(propertyName)) {
                        Object obj = metaObject.getValue(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        Object obj = boundSql.getAdditionalParameter(propertyName);
                        sql = sql.replaceFirst("\\?", getParameterValue(obj));
                    }
                }
            }
        }
        return sql;
    }

    /**
     * 解析参数值用于拼接sql
     *
     * @param obj 参数
     * @return obj.toString()
     */
    private String getParameterValue(Object obj) {
        String value;
        if (obj instanceof String) {
            value = "'" + obj + "'";
        } else if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            value = "'" + formatter.format(new Date()) + "'";
        } else {
            if (obj != null) {
                value = obj.toString();
            } else {
                value = "";
            }
        }
        return value.replace("$", "\\$");
    }
}

拦截器打印效果

2024-09-08 10:48:44.804  INFO 38400 --- [io-12580-exec-3] c.z.config.mybatis.PrintSqlInterceptor   : 
SqlLog ==> statement=cn.zhengzl.mapper.CityMapper.selectList, time=11(ms)
FullSql ==> SELECT id,name,province_id FROM city WHERE (province_id = 44) ORDER BY id ASC
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@3fe9a2fd]

这样我们就得到了一个可以直接复制到可视化工具中直接执行的sql,可以大大节省我们自己替换参数的时间。

备注

mybatis-config.xml文件中的value的取值范围为:SLF4J, LOG4J, LOG4J2, JDK_LOGGING, COMMONS_LOGGING, STDOUT_LOGGING, NO_LOGGING 或实现了org.apache.ibatis.logging.Log的全限定类名。