1. 项目概述MyBatis SQL执行信息打印方案在Java持久层开发中MyBatis因其灵活性和易用性成为主流选择。但在实际开发调试过程中我们经常需要查看MyBatis最终执行的SQL语句、参数绑定情况以及执行耗时等关键信息。虽然MyBatis官方提供了基础的日志输出功能但默认输出往往存在以下痛点日志信息分散在不同日志级别中DEBUG/TRACE缺乏统一的执行时间统计SQL与参数分离导致可读性差重要信息被淹没在大量无关日志中本文将介绍一种通过自定义拦截器实现的控制台SQL监控方案能够清晰输出以下核心信息执行的Mapper方法全限定名实际执行的SQL语句含替换后的参数值精确到毫秒的SQL执行耗时执行结果状态成功/失败这种方案特别适合在以下场景使用开发阶段的SQL调试性能优化时的慢SQL识别生产环境的问题排查需谨慎开启2. 核心实现原理与技术选型2.1 MyBatis拦截器机制解析MyBatis的拦截器Interceptor基于JDK动态代理实现允许在以下4个关键点插入自定义逻辑Executor (update, query, flushStatements, commit, rollback)ParameterHandler (getParameterObject, setParameters)ResultSetHandler (handleResultSets, handleOutputParameters)StatementHandler (prepare, parameterize, batch, update, query)对于SQL监控需求我们主要关注StatementHandler和Executor的拦截Intercepts({ Signature(type StatementHandler.class, method query, args {Statement.class, ResultHandler.class}), Signature(type StatementHandler.class, method update, args {Statement.class}), Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}) }) public class SqlMonitorInterceptor implements Interceptor { // 拦截逻辑实现 }2.2 SQL信息采集关键技术点2.2.1 SQL语句获取通过BoundSql对象可以获取以下关键信息BoundSql boundSql statementHandler.getBoundSql(); String rawSql boundSql.getSql(); // 带?的原始SQL Object parameterObject boundSql.getParameterObject(); // 参数对象2.2.2 参数替换实现将SQL中的?替换为实际参数值需要处理多种情况简单类型参数直接替换Map类型参数按key匹配对象类型参数通过反射获取属性值集合类型参数特殊处理private String replacePlaceholders(String sql, Object parameter) { if (parameter instanceof Map) { // 处理Map类型参数 } else if (parameter ! null) { // 处理对象类型参数 } return sql; }2.2.3 执行时间计算采用System.currentTimeMillis()获取时间戳long start System.currentTimeMillis(); try { return invocation.proceed(); // 执行原方法 } finally { long cost System.currentTimeMillis() - start; // 记录执行时间 }3. 完整实现方案与代码详解3.1 拦截器核心实现类Intercepts({ Signature(type StatementHandler.class, method query, args {Statement.class, ResultHandler.class}), Signature(type StatementHandler.class, method update, args {Statement.class}) }) public class SqlMonitorInterceptor implements Interceptor { private static final Logger logger LoggerFactory.getLogger(SQL-MONITOR); Override public Object intercept(Invocation invocation) throws Throwable { // 获取MappedStatement StatementHandler handler (StatementHandler) invocation.getTarget(); MappedStatement ms (MappedStatement) FieldUtils.readField(handler, delegate.mappedStatement, true); // 获取SQL信息 BoundSql boundSql handler.getBoundSql(); String rawSql boundSql.getSql(); Object parameter boundSql.getParameterObject(); // 记录开始时间 long start System.currentTimeMillis(); try { // 执行原方法 return invocation.proceed(); } finally { // 计算耗时 long cost System.currentTimeMillis() - start; // 格式化输出 String formattedSql formatSql(rawSql, parameter); logger.info(\n SQL监控 \n方法: {}.{}\nSQL: {}\n耗时: {}ms\n, ms.getId().substring(0, ms.getId().lastIndexOf(.)), ms.getId().substring(ms.getId().lastIndexOf(.) 1), formattedSql, cost); } } private String formatSql(String sql, Object parameter) { // 实现参数替换逻辑 // ... } }3.2 Spring Boot集成配置在application.yml中配置mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启MyBatis原生日志 plugins: - com.example.SqlMonitorInterceptor # 注册拦截器或通过Java Config方式Configuration public class MyBatisConfig { Bean public SqlMonitorInterceptor sqlMonitorInterceptor() { return new SqlMonitorInterceptor(); } Bean public ConfigurationCustomizer mybatisConfigurationCustomizer() { return configuration - { configuration.addInterceptor(sqlMonitorInterceptor()); configuration.setLogImpl(StdOutImpl.class); }; } }3.3 日志格式优化方案建议使用Logback的PatternLayout自定义输出格式logger nameSQL-MONITOR levelINFO additivityfalse appender-ref refCONSOLE/ filter classch.qos.logback.classic.filter.ThresholdFilter levelINFO/level /filter /logger4. 高级功能扩展与实践技巧4.1 慢SQL告警机制在拦截器中添加阈值判断// 在finally块中添加 if (cost slowSqlThreshold) { logger.warn(慢SQL警告执行耗时: {}ms, cost); // 可接入邮件/短信告警 }4.2 SQL格式化输出使用第三方库如JSqlParser美化SQLprivate String beautifySql(String sql) { try { Statement statement CCJSqlParserUtil.parse(sql); return statement.toString(); } catch (JSQLParserException e) { return sql; // 解析失败返回原SQL } }4.3 多数据源支持通过ThreadLocal记录数据源信息public class DataSourceContextHolder { private static final ThreadLocalString context new ThreadLocal(); public static void setDataSource(String ds) { context.set(ds); } public static String getDataSource() { return context.get(); } } // 在拦截器中输出数据源信息 String ds DataSourceContextHolder.getDataSource(); logger.info(数据源: {}, ds ! null ? ds : default);5. 生产环境注意事项5.1 性能影响评估拦截器本身会带来约5-15%的性能开销主要来自反射操作获取BoundSql等SQL格式化处理日志IO操作建议在生产环境中通过开关控制是否启用采样率控制如只记录10%的请求异步化日志输出5.2 敏感信息过滤避免输出敏感字段如密码、手机号等private String maskSensitiveData(String sql) { // 使用正则替换敏感信息 return sql.replaceAll((?i)password.*?, password******) .replaceAll(phone\\d{4}\\d{4}, phone*******); }5.3 与现有监控系统集成可将采集的SQL信息发送到Prometheus Grafana 可视化ELK 日志分析系统SkyWalking/Arthas 等APM工具示例Prometheus指标上报Counter.builder(sql_exec_total) .tag(method, methodName) .register(CollectorRegistry.defaultRegistry) .inc(); Summary.builder(sql_exec_time_ms) .quantile(0.5, 0.05) .quantile(0.95, 0.01) .register(CollectorRegistry.defaultRegistry) .observe(cost);6. 常见问题排查指南6.1 拦截器不生效的可能原因配置问题检查拦截器是否被正确注册确认没有其他拦截器覆盖了相同方法Spring代理问题确保拦截器在MyBatis原生拦截器链中检查是否有AOP代理影响了拦截器日志级别问题确认日志级别设置为INFO或更低检查日志框架配置是否正确6.2 SQL参数显示不全的解决方案检查参数对象是否为复杂嵌套结构实现自定义参数格式化器public interface SqlParameterFormatter { String format(Object parameter); } // 注册多个格式化器处理不同类型 MapClass?, SqlParameterFormatter formatters new HashMap(); formatters.put(Date.class, new DateFormatter());6.3 与PageHelper等插件冲突处理当多个拦截器存在时执行顺序很重要明确各拦截器的Order值避免对同一方法的多重拦截在冲突时调整拦截器签名范围典型解决方案AutoConfigureAfter(PageHelperAutoConfiguration.class) public class SqlMonitorInterceptor implements Interceptor { // ... }7. 替代方案对比分析7.1 原生日志配置方案在mybatis-config.xml中配置settings setting namelogImpl valueSTDOUT_LOGGING/ /settings优点零编码实现性能开销小缺点输出格式固定无法获取执行时间参数与SQL分离7.2 第三方监控工具p6spy功能全面支持JDBC层拦截但配置复杂druid内置监控统计功能强大需要切换连接池MyBatis Plus性能分析插件开箱即用但定制能力有限7.3 方案选型建议方案适用场景复杂度性能影响自定义拦截器需要深度定制中中原生日志快速验证低低p6spy全链路监控高高druid已使用该连接池中中对于大多数项目推荐组合使用开发环境自定义拦截器 原生日志测试环境druid监控生产环境采样率控制的拦截器 APM集成