书接上文SqlLogInterceptor mybatis配置打印SQL问题描述在使用 MyBatis-Plus 的saveBatch()和updateBatchById()方法进行批量数据操作时发现自定义的 Druid SQL 日志拦截器SqlLogInterceptor无法打印这些批量操作的 SQL 语句导致调试和问题排查困难。问题分析1. 批量操作的特殊性MyBatis-Plus 的批量操作方法如saveBatch、updateBatchById使用了 JDBC 的批处理Batch模式来提高性能。批处理模式与普通的单条 SQL 执行方式不同普通执行每条 SQL 单独执行会触发statementExecuteAfter、statementExecuteUpdateAfter等方法批处理执行多条 SQL 一起提交执行只会触发statementExecuteBatchAfter方法2. 原有拦截器的问题原有的SqlLogInterceptor继承自 Druid 的FilterEventAdapter实现了以下方法Override protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) { statement.setLastExecuteTimeNano(); printSqlLog(statement, sql); } Override protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) { statement.setLastExecuteTimeNano(); printSqlLog(statement, sql); } Override protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) { statement.setLastExecuteTimeNano(); // 这里没有打印日志 }可以看到statementExecuteBatchAfter方法中只设置了执行时间但没有打印 SQL 日志这就是批量操作 SQL 不打印的根本原因。解决方案方案一修改 Druid 拦截器推荐修改SqlLogInterceptor.java的statementExecuteBatchAfter方法添加 SQL 日志打印逻辑Override protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) { statement.setLastExecuteTimeNano(); // 批量执行后也打印SQL日志 String sql statement.getBatchSql(); if (StringUtil.isNotBlank(sql)) { printSqlLog(statement, sql); } else { // 如果批量SQL为空尝试获取最后执行的SQL sql statement.getLastExecuteSql(); if (StringUtil.isNotBlank(sql)) { printSqlLog(statement, sql); } } }优点统一的日志格式包含执行时间统计可以格式化 SQL 和参数缺点批量操作可能只显示一条 SQL 模板看不到每条具体的参数方案二启用 MyBatis 原生日志在logback.xml配置文件中添加 MyBatis Mapper 的 DEBUG 级别日志!-- MyBatis SQL 日志 -- logger namecom.hzys.mapper levelDEBUG/优点可以看到每条 SQL 的详细参数MyBatis 原生支持稳定可靠缺点日志格式与自定义拦截器不一致日志量较大方案三双管齐下最佳实践同时使用方案一和方案二既能保证批量操作的 SQL 被记录又能在需要时查看详细的参数信息。完整代码示例1. 修改后的 SqlLogInterceptorSlf4j public class SqlLogInterceptor extends FilterEventAdapter { private static final SQLUtils.FormatOption FORMAT_OPTION new SQLUtils.FormatOption(false, false); Override protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) { statement.setLastExecuteTimeNano(); // 批量执行后也打印SQL日志 String sql statement.getBatchSql(); if (StringUtil.isNotBlank(sql)) { printSqlLog(statement, sql); } else { // 如果批量SQL为空尝试获取最后执行的SQL sql statement.getLastExecuteSql(); if (StringUtil.isNotBlank(sql)) { printSqlLog(statement, sql); } } } private void printSqlLog(StatementProxy statement, String sql) { if (!log.isInfoEnabled() || StringUtil.isEmpty(sql)) { return; } try { // 获取参数 int parametersSize statement.getParametersSize(); ListObject parameters new ArrayList(parametersSize); for (int i 0; i parametersSize; i) { parameters.add(getJdbcParameter(statement.getParameter(i))); } // 格式化SQL String dbType statement.getConnectionProxy().getDirectDataSource().getDbType(); String formattedSql SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION); // 打印日志 printSql(formattedSql, statement); } catch (Exception e) { log.error(SQL 格式化失败, e); log.info(\n\n Sql Start \n Execute SQL : {}\n Execute Time: {}\n Sql End \n, sql, StringUtil.format(statement.getLastExecuteTimeNano())); } } private static void printSql(String sql, StatementProxy statement) { String sqlLogger \n\n Sql Start \nExecute SQL : {} \nExecute Time: {} \n Sql End \n; log.info(sqlLogger, sql.trim(), StringUtil.format(statement.getLastExecuteTimeNano())); } }2. Logback 配置?xml version1.0 encodingUTF-8? configuration scantrue scanPeriod60 seconds !-- 其他配置... -- !-- 日志输出级别 -- root levelINFO appender-ref refSTDOUT/ appender-ref refINFO/ appender-ref refWARN/ appender-ref refERROR/ /root !-- MyBatis SQL 日志 -- logger namecom.hzys.mapper levelDEBUG/ logger namenet.sf.ehcache levelINFO/ logger namedruid.sql levelINFO/ /configuration3. Druid 配置Slf4j Configuration public class DruidConfig { Bean Primary ConfigurationProperties(spring.datasource.druid) public DataSource dataSource() { DruidDataSource dataSource DruidDataSourceBuilder.create().build(); // 添加自定义的SQL日志拦截器 SqlLogInterceptor sqlLogInterceptor new SqlLogInterceptor(); dataSource.getProxyFilters().add(sqlLogInterceptor); return dataSource; } }验证效果修改后执行批量操作时会看到类似以下的日志输出 Sql Start Execute SQL : INSERT INTO employee_hourly_rate (id, project_member, member_level, ...) VALUES (?, ?, ?, ...) Execute Time: 15ms Sql End Preparing: INSERT INTO employee_hourly_rate (id, project_member, member_level, ...) VALUES (?, ?, ?, ...) Parameters: 1(Long), 张三(String), 高级工程师(String), ... Parameters: 2(Long), 李四(String), 中级工程师(String), ... Updates: 2注意事项性能考虑DEBUG 级别的 MyBatis 日志会输出大量信息生产环境建议关闭或设置为 INFO 级别日志过滤可以在SqlLogInterceptor中添加过滤逻辑避免打印某些不需要的 SQL如健康检查批量大小MyBatis-Plus 默认批量大小为 1000可以通过配置调整事务管理批量操作需要在事务中执行确保添加Transactional注解总结MyBatis-Plus 批量操作 SQL 不打印的问题主要是因为批处理模式使用了不同的执行路径原有的拦截器没有处理statementExecuteBatchAfter方法。通过修改拦截器并配合 MyBatis 原生日志可以完美解决这个问题既能保证日志的完整性又能在需要时查看详细的执行信息。