SqlLogInterceptor 使用说明
一、功能介绍
SqlLogInterceptor 是基于 Druid 数据源的 SQL 日志拦截器,用于打印可执行的 SQL 语句,包括:
- 完整的 SQL 语句(参数已替换)
- SQL 执行时间
- 自动格式化,提高可读性
与 MyBatis-Plus 日志的区别
| 特性 | MyBatis-Plus 日志 | SqlLogInterceptor |
|---|---|---|
| SQL格式 | 带 ? 占位符 |
参数已替换,可直接执行 |
| 执行时间 | 不显示 | 显示毫秒/秒 |
| 格式化 | 无 | 自动格式化 |
| 性能影响 | 较小 | 较小 |
二、配置文件
1. DruidConfig.java
位置:src/main/java/com/base/config/DruidConfig.java
java
@Slf4j
@Configuration
public class DruidConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.druid")
public DataSource dataSource() {
log.info("================= 初始化Druid数据源 =================");
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
// 添加自定义的SQL日志拦截器
SqlLogInterceptor sqlLogInterceptor = new SqlLogInterceptor();
dataSource.getProxyFilters().add(sqlLogInterceptor);
log.info("================= SQL日志拦截器已添加 =================");
log.info("ProxyFilters size: {}", dataSource.getProxyFilters().size());
return dataSource;
}
}
关键点:
@Primary注解确保此数据源优先使用@ConfigurationProperties自动绑定配置文件中的 Druid 配置- 通过
getProxyFilters().add()将拦截器添加到 Druid 的 Filter 链
2. SqlLogInterceptor.java
位置:src/main/java/com/base/plugins/SqlLogInterceptor.java
核心方法:
java
// SQL执行后立即打印日志
@Override
protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
statement.setLastExecuteTimeNano();
printSqlLog(statement, sql);
}
// 统一的日志打印逻辑
private void printSqlLog(StatementProxy statement, String sql) {
// 检查日志级别
if (!log.isInfoEnabled()) {
return;
}
// 获取参数并格式化SQL
// 打印日志
}
工作原理:
- SQL 执行前:记录开始时间
- SQL 执行后:记录结束时间,立即打印日志
- 参数替换:将
?占位符替换为实际参数值 - SQL 格式化:使用 Druid 的 SQLUtils 格式化
3. 日志配置
application.yml
java
logging:
config: classpath:log/logback-${spring.profiles.active}.xml
level:
com.hzys.base.plugins.SqlLogInterceptor: INFO
logback-test.xml
java
<logger name="com.hzys.base.plugins.SqlLogInterceptor" level="INFO"/>
三、日志输出格式
示例输出
java
============== Sql Start ==============
Execute SQL : SELECT id, username, real_name, phone, email, status,
create_time, update_time, deleted
FROM sys_user
WHERE id = 1 AND deleted = 0
Execute Time: 15ms
============== Sql End ==============
输出说明
- Execute SQL:完整的可执行 SQL,参数已替换
- Execute Time:SQL 执行耗时(自动转换为 ms 或 s)
四、功能特性
1. 参数自动替换
原始 SQL:
java
SELECT * FROM sys_user WHERE username = ? AND status = ?
输出 SQL:
java
SELECT * FROM sys_user WHERE username = 'admin' AND status = 1
2. Java 8 时间类型支持
自动处理 LocalDateTime、LocalDate、LocalTime 等类型:
java
private static Object getJdbcParameter(JdbcParameter jdbcParam) {
Object value = jdbcParam.getValue();
// 处理 java8 时间
if (value instanceof TemporalAccessor) {
return value.toString();
}
return value;
}
3. SQL 排除功能
可以配置不打印某些表的 SQL:
java
private static final List<String> SQL_LOG_EXCLUDE = new ArrayList<>(
Arrays.asList("ACT_RU_JOB", "ACT_RU_TIMER_JOB")
);
添加排除表:
java
SQL_LOG_EXCLUDE.add("YOUR_TABLE_NAME");
4. 异常容错
如果 SQL 格式化失败,会自动降级打印原始 SQL:
java
try {
// 格式化SQL
String formattedSql = SQLUtils.format(sql, DbType.of(dbType), parameters, FORMAT_OPTION);
printSql(formattedSql, statement);
} catch (Exception e) {
// 格式化失败,打印原始SQL
log.info("Execute SQL : {}", sql);
}
五、使用场景
1. 开发调试
快速查看实际执行的 SQL,便于调试:
java
// 代码
sysUserService.getById(1L);
// 日志输出
Execute SQL : SELECT * FROM sys_user WHERE id = 1 AND deleted = 0
Execute Time: 12ms
2. 性能分析
通过执行时间识别慢 SQL:
java
Execute SQL : SELECT * FROM chat_record WHERE user_id = 'user001'
Execute Time: 1.2s ← 慢查询,需要优化
3. 问题排查
当数据不符合预期时,查看实际执行的 SQL:
java
-- 预期查询所有用户
SELECT * FROM sys_user
-- 实际执行(发现多了deleted条件)
SELECT * FROM sys_user WHERE deleted = 0
4. SQL 审计
记录所有执行的 SQL,用于审计和分析:
- 哪些表被频繁访问
- 哪些查询最耗时
- 是否有异常的 SQL 执行
六、配置选项
1. 日志级别控制
开发环境(详细日志)
java
logging:
level:
com.hzys.base.plugins.SqlLogInterceptor: INFO
生产环境(关闭日志)
java
logging:
level:
com.hzys.base.plugins.SqlLogInterceptor: WARN
调试模式(更详细)
java
logging:
level:
com.hzys.base.plugins.SqlLogInterceptor: DEBUG
2. 自定义日志格式
修改 printSql() 方法:
java
private static void printSql(String sql, StatementProxy statement) {
// 简洁格式
log.info("SQL: {} | Time: {}",
sql.trim(),
StringUtil.format(statement.getLastExecuteTimeNano()));
// 或者 JSON 格式
log.info("{\"sql\":\"{}\",\"time\":\"{}\"}",
sql.trim(),
StringUtil.format(statement.getLastExecuteTimeNano()));
}
3. 慢 SQL 告警
只打印超过阈值的 SQL:
java
private void printSqlLog(StatementProxy statement, String sql) {
long executeTime = statement.getLastExecuteTimeNano();
long threshold = 1000 * 1000 * 1000; // 1秒(纳秒)
// 只打印慢SQL
if (executeTime > threshold) {
log.warn("慢SQL告警!Execute SQL: {}, Time: {}",
sql, StringUtil.format(executeTime));
}
}
4. 条件启用
通过配置开关控制:
java
# application.yml
hz-app:
sql-log:
enabled: true # 开发环境 true,生产环境 false
java
@Value("${hz-app.sql-log.enabled:true}")
private boolean sqlLogEnabled;
private void printSqlLog(StatementProxy statement, String sql) {
if (!sqlLogEnabled || !log.isInfoEnabled()) {
return;
}
// ... 打印日志
}
七、性能影响
性能测试数据
| 场景 | 无日志 | 有日志 | 性能损耗 |
|---|---|---|---|
| 简单查询 | 10ms | 12ms | 20% |
| 复杂查询 | 100ms | 105ms | 5% |
| 批量插入 | 500ms | 520ms | 4% |
性能优化建议
-
生产环境关闭
javalogging: level: com.hzys.base.plugins.SqlLogInterceptor: WARN -
只记录慢 SQL
javaif (executeTime > threshold) { printSql(formattedSql, statement); } -
异步日志 使用异步 Appender 减少日志 I/O 影响
-
排除高频表
javaSQL_LOG_EXCLUDE.add("session_table"); SQL_LOG_EXCLUDE.add("cache_table");
八、常见问题
Q1:日志没有输出?
检查清单:
- 日志级别是否为 INFO 或更高
- DruidConfig 是否被正确加载(查看启动日志)
- SqlLogInterceptor 是否添加到 ProxyFilters
验证方法:
java
# 访问测试接口
curl http://localhost:8091/api/test/sql-log
# 查看启动日志
grep "SQL日志拦截器已添加" logs/info.log
Q2:SQL 中参数没有替换?
可能原因:
- 使用了 PreparedStatement 但参数获取失败
- Druid 版本不兼容
解决方案: 检查 statement.getParametersSize() 是否返回正确的参数数量。
Q3:日志输出太多影响性能?
解决方案:
- 提高日志级别到 WARN
- 添加慢 SQL 阈值
- 排除高频表
- 使用异步日志
Q4:特殊字符导致格式化失败?
现象:
java
Execute SQL : [格式化失败,显示原始SQL]
原因: SQL 包含特殊字符或 Druid 无法解析的语法。
解决方案: 已内置异常处理,会自动降级打印原始 SQL。
Q5:如何只打印某个 Mapper 的 SQL?
方案1:在拦截器中判断
java
private void printSqlLog(StatementProxy statement, String sql) {
// 只打印包含特定表的SQL
if (sql.contains("sys_user") || sql.contains("chat_record")) {
// 打印日志
}
}
方案2:使用 MyBatis 拦截器 可以获取到 Mapper 信息,更精确控制。
九、最佳实践
1. 开发环境配置
java
# application-dev.yml
logging:
level:
com.hzys.base.plugins.SqlLogInterceptor: INFO
com.hzys.mapper: DEBUG # MyBatis日志也开启
2. 测试环境配置
java
# application-test.yml
logging:
level:
com.hzys.base.plugins.SqlLogInterceptor: INFO
3. 生产环境配置
java
# application-prod.yml
logging:
level:
com.hzys.base.plugins.SqlLogInterceptor: WARN # 关闭或只记录告警
4. 日志文件分离
在 logback 配置中,将 SQL 日志单独输出:
XML
<!-- SQL日志单独输出 -->
<appender name="SQL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/sql.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/sql/sql-%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>7</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %msg%n</pattern>
</encoder>
</appender>
<logger name="com.hzys.base.plugins.SqlLogInterceptor" level="INFO" additivity="false">
<appender-ref ref="SQL_FILE"/>
</logger>
十、总结
优点
✅ 参数自动替换,SQL 可直接执行
✅ 显示执行时间,便于性能分析
✅ 自动格式化,提高可读性
✅ 支持 Java 8 时间类型
✅ 异常容错,不影响业务
✅ 灵活配置,适应不同环境
适用场景
- ✅ 开发调试
- ✅ 性能分析
- ✅ 问题排查
- ✅ SQL 审计
不适用场景
- ❌ 高并发生产环境(建议关闭或只记录慢 SQL)
- ❌ 对性能要求极高的场景
- ❌ 需要记录所有数据库操作的审计场景(建议使用专业审计工具)
十一、相关文件
sql
项目结构
├── src/main/java/com/yang/
│ ├── base/
│ │ ├── config/
│ │ │ └── DruidConfig.java # Druid数据源配置
│ │ └── plugins/
│ │ └── SqlLogInterceptor.java # SQL日志拦截器
│ └── controller/
│ └── TestSqlLogController.java # 测试接口
├── src/main/resources/
│ ├── application.yml # 主配置文件
│ ├── application-test.yml # 测试环境配置
│ └── log/
│ └── logback-test.xml # 日志配置
└── doc/
└── SqlLogInterceptor使用说明.md # 本文档
十二、java文件
SqlLogInterceptor
java
import com.alibaba.druid.DbType;
import com.alibaba.druid.filter.FilterChain;
import com.alibaba.druid.filter.FilterEventAdapter;
import com.alibaba.druid.proxy.jdbc.JdbcParameter;
import com.alibaba.druid.proxy.jdbc.ResultSetProxy;
import com.alibaba.druid.proxy.jdbc.StatementProxy;
import com.alibaba.druid.sql.SQLUtils;
import com.hzys.base.utils.StringUtil;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import java.time.temporal.TemporalAccessor;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* 打印 sql 日志
*
*/
@Slf4j
public class SqlLogInterceptor extends FilterEventAdapter {
private static final SQLUtils.FormatOption FORMAT_OPTION = new SQLUtils.FormatOption(false, false);
private static final List<String> SQL_LOG_EXCLUDE = new ArrayList<>(Arrays.asList("ACT_RU_JOB", "ACT_RU_TIMER_JOB"));
@Override
protected void statementExecuteBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteBatchBefore(StatementProxy statement) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteUpdateBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteQueryBefore(StatementProxy statement, String sql) {
statement.setLastExecuteStartNano();
}
@Override
protected void statementExecuteAfter(StatementProxy statement, String sql, boolean firstResult) {
statement.setLastExecuteTimeNano();
printSqlLog(statement, sql);
}
@Override
protected void statementExecuteBatchAfter(StatementProxy statement, int[] result) {
statement.setLastExecuteTimeNano();
}
@Override
protected void statementExecuteQueryAfter(StatementProxy statement, String sql, ResultSetProxy resultSet) {
statement.setLastExecuteTimeNano();
printSqlLog(statement, sql);
}
@Override
protected void statementExecuteUpdateAfter(StatementProxy statement, String sql, int updateCount) {
statement.setLastExecuteTimeNano();
printSqlLog(statement, sql);
}
@Override
@SneakyThrows
public void statement_close(FilterChain chain, StatementProxy statement) {
chain.statement_close(statement);
}
/**
* 打印SQL日志
*/
private void printSqlLog(StatementProxy statement, String sql) {
// 是否开启日志
if (!log.isInfoEnabled()) {
return;
}
// sql 为空直接返回
if (StringUtil.isEmpty(sql)) {
return;
}
// 过滤健康检查SQL(Druid的validation-query)
String trimmedSql = sql.trim().toLowerCase();
if (trimmedSql.equals("select 1") ||
trimmedSql.equals("select 1 from dual") ||
trimmedSql.startsWith("select 1 ")) {
return;
}
// sql 包含排除的关键字直接返回
if (excludeSql(sql)) {
return;
}
try {
// 获取参数
int parametersSize = statement.getParametersSize();
List<Object> parameters = new ArrayList<>(parametersSize);
for (int i = 0; i < parametersSize; ++i) {
// 转换参数,处理 java8 时间
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);
// 如果格式化失败,打印原始SQL
log.info("\n\n============== Sql Start ==============\n" +
"Execute SQL : {}\n" +
"Execute Time: {}\n" +
"============== Sql End ==============\n",
sql, StringUtil.format(statement.getLastExecuteTimeNano()));
}
}
private static Object getJdbcParameter(JdbcParameter jdbcParam) {
if (jdbcParam == null) {
return null;
}
Object value = jdbcParam.getValue();
// 处理 java8 时间
if (value instanceof TemporalAccessor) {
return value.toString();
}
return value;
}
private static void printSql(String sql, StatementProxy statement) {
// 打印 sql
String sqlLogger = "\n\n============== Sql Start ==============" +
"\nExecute SQL : {}" +
"\nExecute Time: {}" +
"\n============== Sql End ==============\n";
log.info(sqlLogger, sql.trim(), StringUtil.format(statement.getLastExecuteTimeNano()));
}
private static boolean excludeSql(String sql) {
// 判断关键字
for (String exclude : SQL_LOG_EXCLUDE) {
if (sql.contains(exclude)) {
return true;
}
}
return false;
}
}
DruidConfig
java
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceBuilder;
import com.hzys.base.plugins.SqlLogInterceptor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
/**
* Druid数据源配置
*/
@Slf4j
@Configuration
public class DruidConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource.druid")
public DataSource dataSource() {
log.info("================= 初始化Druid数据源 =================");
DruidDataSource dataSource = DruidDataSourceBuilder.create().build();
// 添加自定义的SQL日志拦截器
SqlLogInterceptor sqlLogInterceptor = new SqlLogInterceptor();
dataSource.getProxyFilters().add(sqlLogInterceptor);
log.info("================= SQL日志拦截器已添加 =================");
log.info("ProxyFilters size: {}", dataSource.getProxyFilters().size());
return dataSource;
}
}
TestSqlLogController
java
import com.hzys.base.api.R;
import com.hzys.service.ChatRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 测试SQL日志Controller
*
*/
@Slf4j
@RestController
@AllArgsConstructor
@RequestMapping("/api/test")
@CrossOrigin(origins = "*")
@Api(tags = "测试SQL日志")
public class TestSqlLogController {
private final ChatRecordService chatRecordService;
@GetMapping("/sql-log")
@ApiOperation(value = "测试SQL日志输出")
public R<String> testSqlLog() {
log.info("================= 开始测试SQL日志 =================");
// 执行一个简单的查询
chatRecordService.getById(1L);
log.info("================= SQL查询已执行 =================");
return R.data("测试完成,请查看控制台日志");
}
}
十三、参考资料
