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 name="SQL-MONITOR" level="INFO" additivity="false"> <appender-ref ref="CONSOLE"/> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>INFO</level> </filter> </logger>4. 高级功能扩展与实践技巧
4.1 慢SQL告警机制
在拦截器中添加阈值判断:
// 在finally块中添加 if (cost > slowSqlThreshold) { logger.warn("慢SQL警告!执行耗时: {}ms", cost); // 可接入邮件/短信告警 }4.2 SQL格式化输出
使用第三方库如JSqlParser美化SQL:
private 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 ThreadLocal<String> 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); } // 注册多个格式化器处理不同类型 Map<Class<?>, 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 name="logImpl" value="STDOUT_LOGGING"/> </settings>优点:
- 零编码实现
- 性能开销小
缺点:
- 输出格式固定
- 无法获取执行时间
- 参数与SQL分离
7.2 第三方监控工具
p6spy:
- 功能全面
- 支持JDBC层拦截
- 但配置复杂
druid内置监控:
- 统计功能强大
- 需要切换连接池
MyBatis Plus性能分析插件:
- 开箱即用
- 但定制能力有限
7.3 方案选型建议
| 方案 | 适用场景 | 复杂度 | 性能影响 |
|---|---|---|---|
| 自定义拦截器 | 需要深度定制 | 中 | 中 |
| 原生日志 | 快速验证 | 低 | 低 |
| p6spy | 全链路监控 | 高 | 高 |
| druid | 已使用该连接池 | 中 | 中 |
对于大多数项目,推荐组合使用:
- 开发环境:自定义拦截器 + 原生日志
- 测试环境:druid监控
- 生产环境:采样率控制的拦截器 + APM集成