在之前的系列文章中,我们完成了平台的基础架构、多数据库分页、DDL/DML补全以及设计模式优化。随着平台服务的用户越来越多(目前已有2000+),一个现实的问题逐渐暴露出来:当用户反馈"我的查询很慢"或者"我明明执行了删除,数据却还在"时,我们没有任何日志可查。 今天这篇文章,我们就为平台引入SQL执行监听器机制,在不侵入核心执行链路的前提下,实现日志记录、慢SQL监控和操作审计。
一、为什么需要监听器?
先来看几个真实场景:
场景一:性能排查
用户反馈:"执行 SELECT * FROM orders WHERE create_time > '2025-01-01' 要等十几秒。"
你问:"你什么时候执行的?完整SQL是什么?参数是什么?"
用户:"就刚才啊,SQL就是我发的这个。"
你:......(没有日志,无从查起)
场景二:操作审计
运维发现某张核心表的数据被删了,需要追查是谁在什么时候执行的 DELETE。
你翻遍代码,发现 StatementHandler 里只有 stmt.executeUpdate(),没有任何记录。
你:......(没有审计日志,无法追溯)
场景三:慢SQL治理
平台需要定期统计哪些SQL执行超过3秒,以便优化或提醒用户。
你发现没有任何地方记录了SQL的执行耗时。
你:......(没有监控数据,无法治理)
这些问题的根源在于:我们的执行链路只关心"把SQL执行完",不关心"谁在什么时候执行了什么,执行了多久,结果如何"。
解决方案就是引入监听器(Listener) 机制:在SQL执行的关键节点插入回调,让外部代码有机会记录、监控和审计,而核心执行逻辑不需要任何改动。
二、设计思路
2.1 监听器应该放在哪一层?
我们的架构分层是:
SqlSession → Executor → StatementHandler → ResultSetHandler
- SqlSession层:太靠上,只负责对外API,不感知具体执行细节。
- StatementHandler层:太靠下,每个方法内部都在操作PreparedStatement,插入监听器会导致代码碎片化。
- Executor层 :刚刚好。Executor是执行器的统一入口,所有SELECT、DML、DDL都经过这里,且它能拿到完整的SQL、参数、命令类型和执行结果。在这里包装监听器,覆盖面最广,侵入性最小。
2.2 监听器的触发时机
我们定义三个核心节点:
| 节点 | 触发时机 | 用途 |
|---|---|---|
| beforeExecute | SQL执行前 | 记录开始时间、SQL、参数、用户信息 |
| afterExecute | SQL执行成功后 | 记录耗时、影响行数、结果集大小 |
| onError | SQL执行抛出异常时 | 记录异常信息、耗时、SQL、参数 |
2.3 监听器链的设计
参考MyBatis的Interceptor链,我们采用责任链模式 :多个监听器按顺序执行,每个监听器都可以独立处理事件。通过 getOrder() 方法控制执行顺序,数值越小越先执行。
三、核心接口设计
3.1 ExecutionEvent(执行事件)
封装一次SQL执行的全部上下文信息。
java
public class ExecutionEvent {
/** 执行的SQL语句 */
private String sql;
/** SQL参数 */
private Object[] parameters;
/** SQL命令类型:SELECT / DML / DDL */
private SqlCommandType commandType;
/** 执行开始时间(毫秒) */
private long startTime;
/** 执行结束时间(毫秒) */
private long endTime;
/** 执行结果(SELECT返回List大小,DML/DDL返回影响行数) */
private int resultCount;
/** 执行异常(仅onError时有值) */
private Throwable error;
/** 当前会话标识(可用于关联用户) */
private String sessionId;
// 计算耗时
public long getCostTime() {
return endTime - startTime;
}
// getter/setter 省略
}
3.2 ExecutionListener(监听器接口)
java
public interface ExecutionListener {
/**
* SQL执行前触发
*/
default void beforeExecute(ExecutionEvent event) {}
/**
* SQL执行成功后触发
*/
default void afterExecute(ExecutionEvent event) {}
/**
* SQL执行异常时触发
*/
default void onError(ExecutionEvent event, Throwable error) {}
/**
* 监听器执行顺序,数值越小越先执行
*/
default int getOrder() {
return 0;
}
}
3.3 ListenerRegistry(监听器注册表)
参考第五篇博客中 DialectRegistry 的设计思想,我们为监听器也设计一个注册表,统一管理监听器的注册、排序和获取。
java
public class ListenerRegistry {
private final List<ExecutionListener> listeners = new CopyOnWriteArrayList<>();
public void addListener(ExecutionListener listener) {
listeners.add(listener);
// 按order排序
listeners.sort(Comparator.comparingInt(ExecutionListener::getOrder));
}
public List<ExecutionListener> getListeners() {
return Collections.unmodifiableList(listeners);
}
public boolean isEmpty() {
return listeners.isEmpty();
}
}
四、与现有架构集成
4.1 扩展Configuration
在 Configuration 中持有 ListenerRegistry,并提供注册监听器的入口。
java
public class Configuration {
private DataSource dataSource;
private DatabaseType databaseType;
private Dialect dialect;
private final ListenerRegistry listenerRegistry = new ListenerRegistry(); // 新增
// ... 原有构造方法 ...
public void addListener(ExecutionListener listener) {
listenerRegistry.addListener(listener);
}
public ListenerRegistry getListenerRegistry() {
return listenerRegistry;
}
}
4.2 改造SimpleExecutor
这是核心改动点。我们在 SimpleExecutor 的每个执行方法中,包装监听器的触发逻辑。
为了避免在每个方法中重复写监听器代码,我们抽取一个模板方法 executeWithListener:
java
public class SimpleExecutor implements Executor {
private final Configuration configuration;
private final StatementHandler statementHandler;
public SimpleExecutor(Configuration configuration) {
this.configuration = configuration;
this.statementHandler = new SimpleStatementHandler(configuration);
}
@Override
public List<Map<String, Object>> query(String sql, Object... parameters) {
ExecutionEvent event = createEvent(sql, parameters, SqlCommandType.SELECT);
return executeWithListener(event, () -> {
List<Map<String, Object>> result = statementHandler.query(sql, parameters);
event.setResultCount(result.size());
return result;
});
}
@Override
public int dml(String sql, Object... parameters) {
if (!configuration.isDmlEnabled()) {
throw new UnsupportedOperationException("DML operations are disabled");
}
ExecutionEvent event = createEvent(sql, parameters, SqlCommandType.DML);
return executeWithListener(event, () -> {
int count = statementHandler.dml(sql, parameters);
event.setResultCount(count);
return count;
});
}
@Override
public int ddl(String sql, Object... parameters) {
if (!configuration.isDdlEnabled()) {
throw new UnsupportedOperationException("DDL operations are disabled");
}
ExecutionEvent event = createEvent(sql, parameters, SqlCommandType.DDL);
return executeWithListener(event, () -> {
int count = statementHandler.ddl(sql, parameters);
event.setResultCount(count);
return count;
});
}
// ==================== 核心模板方法 ====================
private <T> T executeWithListener(ExecutionEvent event, SqlCallable<T> callable) {
ListenerRegistry registry = configuration.getListenerRegistry();
// 无监听器时直接执行,零开销
if (registry.isEmpty()) {
try {
return callable.call();
} catch (SQLException e) {
throw new RuntimeException("SQL execution failed: " + event.getSql(), e);
}
}
// 1. 前置通知
event.setStartTime(System.currentTimeMillis());
for (ExecutionListener listener : registry.getListeners()) {
try {
listener.beforeExecute(event);
} catch (Exception e) {
// 监听器异常不影响主流程,仅记录日志
log.warn("Listener beforeExecute error", e);
}
}
try {
// 2. 执行SQL
T result = callable.call();
event.setEndTime(System.currentTimeMillis());
// 3. 后置通知
for (ExecutionListener listener : registry.getListeners()) {
try {
listener.afterExecute(event);
} catch (Exception e) {
log.warn("Listener afterExecute error", e);
}
}
return result;
} catch (Exception e) {
event.setEndTime(System.currentTimeMillis());
event.setError(e);
// 4. 异常通知
for (ExecutionListener listener : registry.getListeners()) {
try {
listener.onError(event, e);
} catch (Exception ex) {
log.warn("Listener onError error", ex);
}
}
throw new RuntimeException("SQL execution failed: " + event.getSql(), e);
}
}
private ExecutionEvent createEvent(String sql, Object[] parameters, SqlCommandType type) {
ExecutionEvent event = new ExecutionEvent();
event.setSql(sql);
event.setParameters(parameters);
event.setCommandType(type);
return event;
}
@FunctionalInterface
private interface SqlCallable<T> {
T call() throws SQLException;
}
// ... 分页查询、close 等方法保持不变 ...
}
关键设计:监听器执行本身如果抛异常,绝不能影响主流程。因此每个监听器调用都包裹了try-catch,只记录告警日志。
4.3 类图
text
┌─────────────────────────────────────────────────────────────────────────────┐
│ SqlSession │
│ + selectList(sql, params): List<Map> │
│ + executeDML(sql, params): int │
│ + executeDDL(sql, params): int │
└────────────────────────────────┬────────────────────────────────────────────┘
│ 持有
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ SimpleExecutor │
│ - configuration: Configuration │
│ - statementHandler: StatementHandler │
│ + query(sql, params): List<Map> │
│ + dml(sql, params): int │
│ + ddl(sql, params): int │
│ - executeWithListener(event, callable): T ← 核心模板方法 │
└────────────────────────────────┬────────────────────────────────────────────┘
│ 使用
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ListenerRegistry │
│ - listeners: List<ExecutionListener> │
│ + addListener(listener): void │
│ + getListeners(): List<ExecutionListener> │
│ + isEmpty(): boolean │
└────────────────────────────────┬────────────────────────────────────────────┘
│ 持有
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ ExecutionListener (接口) │
│ + beforeExecute(event): void │
│ + afterExecute(event): void │
│ + onError(event, error): void │
│ + getOrder(): int │
└────────────────────────────────┬────────────────────────────────────────────┘
│ 实现
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ LoggingExecutionListener │
│ SlowSqlExecutionListener │
│ AuditExecutionListener │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────┐
│ Configuration │
│ - listenerRegistry: ListenerRegistry │
│ + addListener(listener): void │
│ + getListenerRegistry(): ListenerRegistry │
└─────────────────────────────────────────────────────────────────────────────┘
五、内置监听器实现
5.1 日志监听器(LoggingExecutionListener)
记录每次SQL执行的完整信息,便于排查问题。
java
public class LoggingExecutionListener implements ExecutionListener {
private static final Logger log = LoggerFactory.getLogger(LoggingExecutionListener.class);
@Override
public void beforeExecute(ExecutionEvent event) {
log.info("[SQL-BEFORE] type={}, sql={}, params={}",
event.getCommandType(),
event.getSql(),
Arrays.toString(event.getParameters()));
}
@Override
public void afterExecute(ExecutionEvent event) {
log.info("[SQL-AFTER] type={}, cost={}ms, resultCount={}, sql={}",
event.getCommandType(),
event.getCostTime(),
event.getResultCount(),
event.getSql());
}
@Override
public void onError(ExecutionEvent event, Throwable error) {
log.error("[SQL-ERROR] type={}, cost={}ms, sql={}, params={}, error={}",
event.getCommandType(),
event.getCostTime(),
event.getSql(),
Arrays.toString(event.getParameters()),
error.getMessage(), error);
}
@Override
public int getOrder() {
return 0; // 最先执行
}
}
5.2 慢SQL监听器(SlowSqlExecutionListener)
只关注执行时间超过阈值的SQL,用于性能监控和告警。
java
public class SlowSqlExecutionListener implements ExecutionListener {
private static final Logger log = LoggerFactory.getLogger(SlowSqlExecutionListener.class);
private final long thresholdMs;
public SlowSqlExecutionListener(long thresholdMs) {
this.thresholdMs = thresholdMs;
}
@Override
public void afterExecute(ExecutionEvent event) {
if (event.getCostTime() > thresholdMs) {
log.warn("[SLOW-SQL] cost={}ms (threshold={}ms), type={}, sql={}, params={}",
event.getCostTime(),
thresholdMs,
event.getCommandType(),
event.getSql(),
Arrays.toString(event.getParameters()));
// 这里可以扩展:发送告警邮件、写入监控系统、推送钉钉/企微消息
}
}
@Override
public int getOrder() {
return 10;
}
}
5.3 审计监听器(AuditExecutionListener)
记录所有DML和DDL操作,用于安全审计。这是平台最需要的功能之一。
java
public class AuditExecutionListener implements ExecutionListener {
private static final Logger auditLog = LoggerFactory.getLogger("AUDIT");
private final AuditRepository auditRepository; // 审计日志存储(可写数据库或文件)
public AuditExecutionListener(AuditRepository auditRepository) {
this.auditRepository = auditRepository;
}
@Override
public void afterExecute(ExecutionEvent event) {
// 只审计DML和DDL,SELECT不记录(量太大)
if (event.getCommandType() == SqlCommandType.SELECT) {
return;
}
AuditRecord record = new AuditRecord();
record.setSessionId(event.getSessionId());
record.setCommandType(event.getCommandType().name());
record.setSql(event.getSql());
record.setParameters(Arrays.toString(event.getParameters()));
record.setResultCount(event.getResultCount());
record.setCostTime(event.getCostTime());
record.setExecuteTime(new Date(event.getStartTime()));
record.setSuccess(true);
auditRepository.save(record);
auditLog.info("[AUDIT] session={}, type={}, sql={}, resultCount={}, cost={}ms",
event.getSessionId(),
event.getCommandType(),
event.getSql(),
event.getResultCount(),
event.getCostTime());
}
@Override
public void onError(ExecutionEvent event, Throwable error) {
if (event.getCommandType() == SqlCommandType.SELECT) {
return;
}
AuditRecord record = new AuditRecord();
record.setSessionId(event.getSessionId());
record.setCommandType(event.getCommandType().name());
record.setSql(event.getSql());
record.setParameters(Arrays.toString(event.getParameters()));
record.setCostTime(event.getCostTime());
record.setExecuteTime(new Date(event.getStartTime()));
record.setSuccess(false);
record.setErrorMessage(error.getMessage());
auditRepository.save(record);
}
@Override
public int getOrder() {
return 20;
}
}
六、使用示例
6.1 注册监听器
java
DataSource dataSource = ...;
Configuration configuration = new Configuration(dataSource);
// 注册日志监听器
configuration.addListener(new LoggingExecutionListener());
// 注册慢SQL监听器(阈值3秒)
configuration.addListener(new SlowSqlExecutionListener(3000));
// 注册审计监听器
configuration.addListener(new AuditExecutionListener(auditRepository));
try (SqlSession session = new SqlSession(configuration)) {
// 执行查询
session.selectList("SELECT * FROM user WHERE age > ?", 18);
// 执行DML(会被审计)
session.executeDML("UPDATE user SET age = ? WHERE id = ?", 26, 1);
// 执行DDL(会被审计)
session.executeDDL("CREATE TABLE test(id INT)");
}
6.2 日志输出示例
text
[SQL-BEFORE] type=SELECT, sql=SELECT * FROM user WHERE age > ?, params=[18]
[SQL-AFTER] type=SELECT, cost=45ms, resultCount=23, sql=SELECT * FROM user WHERE age > ?
[SLOW-SQL] cost=5230ms (threshold=3000ms), type=SELECT, sql=SELECT * FROM orders WHERE ..., params=[]
[AUDIT] session=abc123, type=DML, sql=UPDATE user SET age = ? WHERE id = ?, resultCount=1, cost=12ms
七、总结
7.1 零开销设计
当 ListenerRegistry 为空时,executeWithListener 直接执行SQL,没有任何额外的性能开销。只有注册了监听器,才会有回调逻辑。这对于不需要监听器的场景非常友好。
7.2 监听器异常隔离
每个监听器的调用都包裹了 try-catch。如果某个监听器抛异常(比如写审计日志时数据库连接失败),不会影响主流程的SQL执行,只会记录一条告警日志。这是监听器机制必须遵守的原则。
7.3 与现有架构无缝集成
监听器没有改变 StatementHandler、ResultSetHandler 的任何代码,也没有改变 SqlSession 的对外API。它只是在 Executor 层做了一层"包装",完全符合开闭原则。
7.4 可扩展性
新增监听器只需实现 ExecutionListener 接口,注册到 Configuration 即可。
通过 getOrder() 控制多个监听器的执行顺序。
审计监听器可以对接任意存储(数据库、文件、Elasticsearch、消息队列)。