Mybatis自定义日志打印

一,目标

  1.  替换?为具体的参数值
  2. 统计sql执行时间
  3. 记录执行时间过长的sql,并输出信息到文档(以天为单位进行存储)

平常打印出来的sql都是sql一行,参数一行。如图:

二,理论

 这里我们主要通过Mybatis的Interceptor接口与Java自身的IO操作来实现。

Interceptor接口

MyBatis 的 Interceptor 是一个强大的功能,它允许开发者在执行数据库操作的过程中插入自定义逻辑。通过使用拦截器,我们可以在执行 SQL 语句之前或之后进行处理,记录日志、修改输入参数、管理事务、监控性能等功能。

@Intercepts注解

Mybatis提供的一个用于定义拦截器的注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Intercepts {
    Signature[] value(); //这里表示可以添加多个注解Signature作为value
}

@Intercepts注解

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
    Class<?> type(); // 拦截哪个类

    String method(); // 类的哪个方法

    Class<?>[] args(); // 什么参数
}

这里需要注意一点

方法和参数与你当前项目中引入的版本有关。 写时请必须确认你写的类中确实有该方法,该参数。否则就会出现类似

### Error opening session. Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object) ### Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query. Cause: java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object)] with root cause java.lang.NoSuchMethodException: org.apache.ibatis.executor.Executor.query(org.apache.ibatis.mapping.MappedStatement,java.lang.Object)

的异常。

三,实操

1, 核心类--SqlPrintInterceptor

package com.luojie.config.myInterface.mybatisIntercept;

import com.luojie.common.Conditions;
import com.luojie.util.TxtUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;

/**
 * 自定义打印日志功能的拦截器
 */
@Intercepts({
        // 拦截 Executor 接口的 query 方法,包含不同的参数组合
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "queryCursor", args = {MappedStatement.class, Object.class, RowBounds.class}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Slf4j
public class SqlPrintInterceptor implements Interceptor {

    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 记录开始时间
        long startTime = System.currentTimeMillis();
        Object proceed = null;

        // 执行原始方法
        try {
            proceed = invocation.proceed();
        } catch (Throwable t) {
            log.error("Error during SQL execution", t);
            throw t; // 重新抛出异常
        }

        // 记录结束时间
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime; // 计算执行时间

        // 转换执行时间为 "XXs.XXms" 格式
        String formattedExecutionTime = formatExecutionTime(executionTime);

        // 生成打印的 SQL 语句
        String printSql = generateSql(invocation);
        // 输出 SQL 和执行时间
        System.out.println(Conditions.RED + "SQL: " + printSql);
        System.out.println("Execution time: " + formattedExecutionTime);
        System.out.print(Conditions.RESET);
        log.info("SQL: " + printSql);
        log.info("Execution time: " + formattedExecutionTime);

        // 记录慢sql(这里我为了方便观察,所以设置界限为0,各位可以根据实际情况设置)
        if ((executionTime / 1000) >= 0) {
            writeSlowSqlToLocation(printSql, formattedExecutionTime);
        }

        return proceed; // 返回原始方法的结果
    }

    // 记录慢sql
    private void writeSlowSqlToLocation(String sql, String executeTime) {
        String formattedDate = dateFormat.format(new Date());
        String logs = formattedDate + "  SQL: " + sql + "  执行耗时: " + executeTime;
        TxtUtil.writeLog(logs);
    }


    // 新增格式化执行时间的方法
    private String formatExecutionTime(long executionTime) {
        long seconds = executionTime / 1000; // 获取秒数
        long milliseconds = executionTime % 1000; // 获取剩余的毫秒数
        return String.format("%ds.%03dms", seconds, milliseconds); // 格式化为 "XXs.XXXms"
    }

    private String generateSql(Invocation invocation) {
        // 获取 MappedStatement 对象
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        Object parameter = null;

        // 获取参数对象
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }

        // 获取 MyBatis 配置
        Configuration configuration = mappedStatement.getConfiguration();
        // 获取 BoundSql 对象
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);

        // 获取参数对象
        Object parameterObject = boundSql.getParameterObject();
        // 获取参数映射列表
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // 获取执行的 SQL 语句
        String sql = boundSql.getSql();

        // 替换 SQL 中多个空格为一个空格
        sql = sql.replaceAll("[\\s]+", " ");

        // 如果参数对象和参数映射不为空
        if (!ObjectUtils.isEmpty(parameterObject) && !ObjectUtils.isEmpty(parameterMappings)) {
            // 如果只有一个参数,直接替换
            if (parameterObject instanceof String && parameterMappings.size() == 1) {
                return sql.replaceFirst("\\?", String.valueOf(parameterObject)); // 处理缺少值的情况
            }
            // 遍历每个参数映射
            for (ParameterMapping parameterMapping : parameterMappings) {
                String propertyName = parameterMapping.getProperty(); // 获取属性名
                MetaObject metaObject = configuration.newMetaObject(parameterObject); // 创建 MetaObject

                Object obj = null; // 初始化参数对象
                // 如果参数对象有对应的 getter 方法
                if (metaObject.hasGetter(propertyName)) {
                    obj = metaObject.getValue(propertyName); // 获取参数值
                } else if (boundSql.hasAdditionalParameter(propertyName)) {
                    obj = boundSql.getAdditionalParameter(propertyName); // 获取附加参数
                }

                // 替换 SQL 中的占位符
                if (obj != null) {
                    sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(obj)));
                } else {
                    sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(propertyName)); // 处理缺少值的情况
                }
            }
        }
        return sql; // 返回生成的 SQL 语句
    }

    private String getParameterValue(Object parameterObject) {
        // 如果参数对象为空,返回 "null"
        if (parameterObject == null) {
            return "null";
        }
        // 返回参数对象的字符串表示
        return parameterObject.toString();
    }

    @Override
    public Object plugin(Object target) {
        // 生成插件对象
        return Interceptor.super.plugin(target);
    }

    @Override
    public void setProperties(Properties properties) {
        // 设置属性
        Interceptor.super.setProperties(properties);
    }
}

2, txt工具类--TxtUtil

package com.luojie.util;

import lombok.extern.slf4j.Slf4j;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 简单的txt文件工具 类
 *
 */
@Slf4j
public class TxtUtil {

    private static final String LOG_DIRECTORY = "D:\\tmp\\log"; // 指定日志文件夹
    private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

    public static void writeLog(String message) {
        // 获取当前日期
        String currentDate = dateFormat.format(new Date());
        // 构建文件路径
        String filePath = LOG_DIRECTORY + "/" + currentDate + ".txt";

        // 创建目录如果不存在
        try {
            Files.createDirectories(Paths.get(LOG_DIRECTORY));
        } catch (IOException e) {
            log.error(e.getMessage());
        }

        // 追加写入文件
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) {
            writer.write(message);
            writer.newLine(); // 换行
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }
}

3, 常量类--Conditions

package com.luojie.common;

/**
 * 常量类
 */
public class Conditions {
    /**
     * 控制print的打印颜色
     */
    public static final String RED = "\033[0;31m";  // 红色
    public static final String GREEN = "\033[0;32m"; // 绿色
    public static final String YELLOW = "\033[0;33m"; // 黄色
    public static final String BLUE = "\033[0;34m"; // 蓝色
    public static final String MAGENTA = "\033[0;35m"; // 品红
    public static final String CYAN = "\033[0;36m"; // 青色
    public static final String WHITE = "\033[0;37m"; // 白色

    /**
     * 重置print所有颜色
     */
    public static final String RESET = "\033[0m";  // 重置
}

这里是为了控制打印到控制台的颜色,方便观察

4, 让mybatis使用上这个拦截器

package com.luojie.config;

import com.luojie.config.myInterface.mybatisIntercept.SqlPrintInterceptor;
import com.zaxxer.hikari.HikariDataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

import javax.sql.DataSource;

@Configuration
@MapperScan(basePackages = "com.luojie.dao.mapper1", sqlSessionFactoryRef = "sqlSessionFactory1")
public class DataSource1Config {

    @Value("${datasource1.url}")
    private String url;
    @Value("${datasource1.username}")
    private String username;
    @Value("${datasource1.password}")
    private String password;

    @Bean(name = "dataSource1")
    public DataSource dataSource1() {
        return DataSourceBuilder.create()
                .url(url)
                .username(username)
                .password(password)
                // 使用HikariCP数据连接池管理
                .type(HikariDataSource.class)
                .build();
    }

    @Bean(name = "sqlSessionFactory1")
    public SqlSessionFactory sqlSessionFactory1(@Qualifier("dataSource1") DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
        sessionFactoryBean.setDataSource(dataSource);
        sessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:rojerTestMapper/mapper1/*.xml"));
        // 增加自定义的sql日志打印器
        // 用new Interceptor[]{new SqlPrintInterceptor()}而不是直接new SqlPrintInterceptor()是为了后续方便扩展
        sessionFactoryBean.setPlugins(new Interceptor[]{new SqlPrintInterceptor()});
        return sessionFactoryBean.getObject();
    }
}

5, 测试结果

只有一个参数时

存在多个参数时

文件保存确认

四, 扩展

我这里简单将慢sql信息记录到本地,大家可以根据自己的项目需要,通过消息中间件实现和短信发送的方式,实现慢sql监控告警等功能。

以上。

祝各位大佬前途光明。

### MyBatis打印日志的原因及解决方案 #### 原因分析 MyBatis未正常打印SQL日志可能由以下几个原因引起: 1. **日志级别错误**:如果`logger`的日志级别不是`DEBUG`,即使进行了相关配置,MyBatis也无法打印SQL日志[^1]。 2. **版本差异**:不同版本的MyBatis日志框架的支持和配置方式有所不同。例如,MyBatis 3.0.6、3.1.0 和 3.2.0 及更高版本之间的日志配置方法存在显著区别。 3. **日志框架冲突**:当项目中同时存在多个日志框架(如SLF4J、Commons Logging等),而没有正确配置桥接器时,可能导致日志无法正常输出。 4. **配置文件缺失或错误**:在Spring Boot项目中,若`application.yml`中的日志配置有误,则会影响SQL日志打印效果[^2][^3]。 --- #### 解决方案 以下是针对MyBatis打印日志问题的具体解决办法: ##### 方法一:调整日志级别至`DEBUG` 确保MyBatis对应的`logger`被设置为`DEBUG`级别。可以通过以下方式进行配置: - 如果使用Log4j作为日志框架,可以在`log4j.properties`中添加如下内容: ```properties log4j.logger.org.mybatis=DEBUG log4j.logger.java.sql=DEBUG ``` - 对于Spring Boot项目,可在`application.yml`中指定日志级别: ```yaml logging: level: org.mybatis: DEBUG java.sql: DEBUG ``` 此操作可确保MyBatis及其内部组件能够记录详细的SQL执行信息^。 ##### 方法二:显式指定日志实现 通过`mybatis.configuration.log-impl`参数强制指定日志实现类。例如,在`application.yml`中加入以下配置: ```yaml mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl ``` 该配置会将SQL日志直接输出到控制台,适用于快速排查问题场景[^2]^。 ##### 方法三:结合具体日志框架进行配置 根据不同项目的实际需求选择适合的日志框架并完成相应配置: - 使用Log4j2时,可以这样配置: ```yaml mybatis: configuration: log-impl: org.apache.ibatis.logging.log4j2.Log4j2Impl ``` - 若采用自定义日志逻辑,也可以在启动类中手动调用`LogFactory.useCustomLogging()`方法: ```java import org.apache.ibatis.logging.LogFactory; public class Application { static { LogFactory.useCustomLogging(StdOutImpl.class); } } ``` 这种方法特别适合那些已经集成了特定日志系统的复杂工程环境[^4]^。 ##### 方法四:处理多日志框架共存的情况 当项目依赖了多种日志库时,需引入适当的桥接Jar包以消除潜在冲突。比如,对于SLF4J与Log4j组合的情形,应包含`slf4j-log4j12.jar`;而对于Logback则需要`slf4j-logback-classic.jar`等[^1]^。 --- ### 总结 综上所述,要成功使MyBatis打印SQL日志,不仅要注意基础配置是否恰当,还需兼顾所选日志框架特性以及项目整体结构特点做出针对性修改。只有做到精确匹配各要素间关系,才能从根本上杜绝此类异常现象的发生。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值