Spring Boot中如何记录所有执行SQL及递归调用栈信息?
Spring Boot中记录SQL查询及完整调用链的实现方案
以下针对Spring Boot中最常用的两种持久层框架,提供具体实现方案:
一、基于MyBatis的实现方式
1. 自定义MyBatis拦截器拦截SQL执行
通过实现MyBatis的Interceptor接口,拦截SQL执行过程,获取最终执行的SQL语句,同时解析当前线程的调用栈生成业务层到持久层的调用链。
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})}) @Component public class SqlLoggingInterceptor implements Interceptor { private static final Logger logger = LoggerFactory.getLogger(SqlLoggingInterceptor.class); @Override public Object intercept(Invocation invocation) throws Throwable { // 获取最终执行的SQL语句 StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); BoundSql boundSql = statementHandler.getBoundSql(); String sql = boundSql.getSql().trim(); // 解析调用栈,提取Service和Mapper/Repository的调用节点 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); List<String> callChain = new ArrayList<>(); for (StackTraceElement element : stackTrace) { String className = element.getClassName(); String methodName = element.getMethodName(); // 根据项目包结构筛选业务层和持久层类,可自行调整匹配规则 if (className.contains(".service.") || className.contains(".mapper.") || className.contains(".repository.")) { String simpleClassName = className.substring(className.lastIndexOf('.') + 1); callChain.add(simpleClassName + "." + methodName + "()"); } } // 反转调用链(栈顺序是从当前方法往上,反转后得到Service -> Mapper的调用顺序) Collections.reverse(callChain); // 组装日志格式并输出 StringBuilder logContent = new StringBuilder(); for (String callNode : callChain) { logContent.append(callNode).append(" > "); } logContent.append(sql).append(";"); logger.info(logContent.toString()); // 继续执行原SQL逻辑 return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { // 可配置开关、过滤规则等参数 } }
2. 关键注意点
- 包路径匹配规则:需根据自己项目的包结构调整
className.contains的判断条件,确保只捕获业务层和持久层的调用节点。 - 性能优化:获取调用栈会有一定性能开销,生产环境可通过配置项控制是否启用该拦截器,或者仅在调试阶段开启。
- 动态SQL处理:MyBatis的
BoundSql已经包含解析后的最终SQL,无需额外处理动态拼接的逻辑。
二、基于Spring Data JPA的实现方式
如果使用JPA作为持久层,可以通过Hibernate的拦截器捕获SQL,并结合调用栈生成调用链。
1. 自定义Hibernate拦截器
@Component public class JpaSqlLoggingInterceptor extends EmptyInterceptor { private static final Logger logger = LoggerFactory.getLogger(JpaSqlLoggingInterceptor.class); @Override public String onPrepareStatement(String sql) { // 解析调用栈,提取Service和Repository的调用节点 StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); List<String> callChain = new ArrayList<>(); for (StackTraceElement element : stackTrace) { String className = element.getClassName(); String methodName = element.getMethodName(); if (className.contains(".service.") || className.contains(".repository.")) { String simpleClassName = className.substring(className.lastIndexOf('.') + 1); callChain.add(simpleClassName + "." + methodName + "()"); } } // 反转调用链并组装日志 Collections.reverse(callChain); StringBuilder logContent = new StringBuilder(); for (String callNode : callChain) { logContent.append(callNode).append(" > "); } logContent.append(sql).append(";"); logger.info(logContent.toString()); return super.onPrepareStatement(sql); } }
2. 配置Hibernate启用拦截器
在application.yml中添加配置:
spring: jpa: properties: hibernate.ejb.interceptor: com.yourpackage.JpaSqlLoggingInterceptor
3. 关键注意点
- 版本兼容:不同版本的Hibernate拦截器API可能略有差异,需根据项目使用的版本调整。
- SQL覆盖范围:该拦截器可捕获JPA原生SQL查询和JPQL解析后的最终SQL。
通用优化建议
- 调用栈过滤:可以进一步过滤掉Spring框架自身的方法(如
org.springframework开头的类),避免调用链中混入无关节点。 - SQL格式化:如果需要格式化SQL,可引入第三方库(如
com.github.vertical-blank:sql-formatter)对SQL进行格式化后再输出。 - 开关控制:通过
@Value注入配置项,动态开启或关闭日志功能,避免影响生产环境性能。
内容的提问来源于stack exchange,提问作者Miladro
相关产品推荐
相关产品推荐

