1. 项目背景与核心需求
在Java持久层开发中,MyBatis作为主流ORM框架被广泛使用。但默认配置下,开发者在调试阶段往往难以直观获取SQL执行详情,这给问题排查和性能优化带来不小困扰。最近我在重构一个订单管理系统时,就遇到了需要精确掌握每条SQL执行情况的需求。
通过控制台输出SQL执行信息(包含执行方法名、完整SQL语句、执行耗时)是最直接的调试手段。这不仅能帮助快速定位N+1查询等性能问题,还能在代码评审时验证SQL是否符合预期。下面分享我经过多个项目验证的完整实现方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 实现方案选型与对比
2.1 常见SQL打印方案对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Log4j日志配置 | 零代码侵入 | 无法获取执行方法上下文 | 简单调试环境 |
| MyBatis原生日志实现 | 官方支持 | 输出格式不可定制 | 基础需求 |
| 拦截器方案 | 全链路信息获取 | 需编写代码 | 需要完整执行上下文的场景 |
| P6Spy代理 | 支持多数据源 | 配置复杂 | 生产环境诊断 |
2.2 最终选择:拦截器方案
经过对比测试,我选择了基于Interceptor接口的自定义实现方案。原因有三:
- 可以获取Mapper接口方法名等上下文信息
- 能精确计算SQL执行时间(含网络耗时)
- 输出格式完全可自定义
重要提示:生产环境建议配合日志框架使用,避免直接System.out影响性能
3. 完整实现步骤
3.1 核心拦截器实现
java复制@Intercepts({
@Signature(type = StatementHandler.class,
method = "query",
args = {Statement.class, ResultHandler.class}),
@Signature(type = StatementHandler.class,
method = "update",
args = {Statement.class})
})
public class SqlPrintInterceptor implements Interceptor {
private static final ThreadLocal<Long> START_TIME = new ThreadLocal<>();
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取执行方法信息
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
String methodName = ms.getId();
// 记录开始时间
START_TIME.set(System.currentTimeMillis());
try {
// 执行SQL
Object result = invocation.proceed();
// 计算耗时
long costTime = System.currentTimeMillis() - START_TIME.get();
// 获取SQL语句
BoundSql boundSql = ((StatementHandler)invocation.getTarget())
.getBoundSql();
String sql = boundSql.getSql();
// 格式化输出
printSqlInfo(methodName, sql, costTime);
return result;
} finally {
START_TIME.remove();
}
}
private void printSqlInfo(String method, String sql, long cost) {
System.out.println("\n=== SQL执行报告 ===");
System.out.println("执行方法: " + method);
System.out.println("执行SQL: " + sql.replaceAll("[\\s]+", " "));
System.out.println("执行耗时: " + cost + "ms");
System.out.println("=================\n");
}
}
3.2 Spring Boot配置方式
yaml复制# application.yml
mybatis:
configuration:
plugins:
- com.example.interceptor.SqlPrintInterceptor
3.3 传统SSM配置方式
xml复制<!-- mybatis-config.xml -->
<plugins>
<plugin interceptor="com.example.interceptor.SqlPrintInterceptor"/>
</plugins>
4. 高级功能扩展
4.1 SQL美化输出
在printSqlInfo方法中增加SQL格式化逻辑:
java复制private String formatSql(String originSql) {
return new BasicFormatterImpl().format(originSql)
.replaceAll("\n", "\n\t\t");
}
4.2 慢SQL告警
java复制// 在printSqlInfo方法中添加
if(costTime > 500) { // 超过500ms视为慢查询
System.err.println("⚠️ 慢SQL告警: 执行耗时 " + costTime + "ms");
}
4.3 与Log4j2集成
java复制private static final Logger logger = LogManager.getLogger();
private void printSqlInfo(String method, String sql, long cost) {
logger.info("\n=== SQL执行报告 ===\n" +
"执行方法: {}\n" +
"执行SQL: {}\n" +
"执行耗时: {}ms\n" +
"=================",
method,
sql.replaceAll("[\\s]+", " "),
cost);
}
5. 生产环境注意事项
- 性能影响:拦截器会带来约5-10%的性能损耗,建议通过开关控制
java复制@Value("${sql.print.enable:false}")
private boolean enablePrint;
- 敏感信息过滤:避免打印出密码等敏感字段
java复制private String filterSensitive(String sql) {
return sql.replaceAll("password='.*?'", "password='******'");
}
- 批量操作优化:对于批量insert/update需要特殊处理
java复制if(sql.matches("(?i)insert.*values.*\\(.*\\),.*\\(.*\\)")) {
System.out.println("批量操作: 共" + sql.split("\\),\\s*\\(").length + "条");
}
6. 常见问题排查
6.1 拦截器不生效
检查点:
- 确认插件配置位置正确(MyBatis配置优先于Spring配置)
- 检查拦截的方法签名是否匹配
- 确保没有其他拦截器冲突
6.2 SQL日志重复打印
可能原因:
- 同时配置了Log4j的SQL日志和本拦截器
- 多个数据源重复配置
解决方案:
properties复制# 在log4j配置中关闭MyBatis原生日志
log4j.logger.java.sql=OFF
6.3 特殊字符显示异常
处理方案:
java复制// 在输出前进行转义处理
StringEscapeUtils.escapeJava(sql)
7. 性能优化建议
- 异步日志输出:使用Disruptor等高性能队列
java复制private static final ExecutorService logExecutor =
Executors.newSingleThreadExecutor();
logExecutor.submit(() -> {
// 日志输出逻辑
});
- 采样率控制:非全量打印
java复制private boolean shouldPrint() {
return ThreadLocalRandom.current().nextInt(100) < 10; // 10%采样
}
- SQL指纹统计:相同SQL去重
java复制private static final ConcurrentHashMap<String, AtomicInteger> SQL_COUNTER
= new ConcurrentHashMap<>();
String sqlFingerprint = DigestUtils.md5Hex(sql);
SQL_COUNTER.computeIfAbsent(sqlFingerprint, k -> new AtomicInteger(0))
.incrementAndGet();
经过多个项目的实践验证,这套方案在开发调试阶段能极大提升效率。特别是在处理复杂业务逻辑时,清晰的SQL执行日志可以帮助快速定位数据不一致问题。建议根据实际项目需求调整输出格式和内容粒度
