手写MyBatis第63弹:MyBatis SQL日志插件完整实现:专业级SQL监控与调试方案
🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我。
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
目录
正文
一、SQL日志插件的重要性与设计原则
1. 为什么需要专业的SQL日志插件
2. 设计原则
二、插件核心实现方案
1. 插件拦截点选择
2. 插件执行流程设计
三、完整SQL日志插件实现
1. 插件核心结构
2. SQL执行上下文管理
3. Executor方法处理
4. StatementHandler方法处理
四、参数格式化与安全处理
1. 参数格式化实现
2. 安全参数处理
3. 敏感数据脱敏
五、完整的日志输出格式
1. 查询操作日志输出
2. 更新操作日志输出
3. 错误日志输出
六、高级特性实现
1. 执行时间监控与慢SQL告警
2. SQL执行统计
3. 动态日志级别控制
七、生产环境配置建议
1. 日志配置文件示例
2. 插件配置优化
八、为什么分离输出Preparing和Parameters
1. 安全性考虑
2. 调试便利性
3. 性能优化
九、MyBatis标准日志输出格式
十、总结
-
MyBatis SQL日志插件完整实现:专业级SQL监控与调试方案
-
手把手打造MyBatis日志插件:支持增删改查的全功能SQL日志
-
MyBatis SQL日志深度优化:从简单输出到企业级监控解决方案
-
全面掌握MyBatis日志插件开发:安全、性能、功能三位一体
-
MyBatis专业SQL日志实践:Preparing与Parameters分离输出的设计哲学
正文
在MyBatis应用开发和维护过程中,SQL日志是调试性能问题、排查业务异常不可或缺的工具。一个专业的SQL日志插件不仅能输出执行的SQL语句,还能提供参数信息、执行时间、结果集大小等关键信息。本文将深入探讨如何实现一个完整的MyBatis SQL日志插件,涵盖从基础功能到高级特性的全方位解决方案。
一、SQL日志插件的重要性与设计原则
1. 为什么需要专业的SQL日志插件
-
调试与排查:快速定位SQL执行问题和性能瓶颈
-
审计与监控:记录所有数据库操作,满足合规要求
-
性能分析:统计SQL执行时间和频率,识别优化点
-
安全审计:监控异常数据访问模式
2. 设计原则
-
完整性:支持所有类型的SQL操作(增删改查)
-
安全性:避免输出敏感信息,支持数据脱敏
-
性能友好:最小化对业务性能的影响
-
可配置性:支持动态开启关闭和详细程度控制
二、插件核心实现方案
1. 插件拦截点选择
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})public class SqlLoggingPlugin implements Interceptor {// 插件实现}
2. 插件执行流程设计
三、完整SQL日志插件实现
1. 插件核心结构
public class ComprehensiveSqlLoggingPlugin implements Interceptor {private static final Logger logger = LoggerFactory.getLogger("SQL_LOGGER");// 线程局部变量存储执行上下文private static final ThreadLocal<SqlExecutionContext> contextHolder = new ThreadLocal<>();@Overridepublic Object intercept(Invocation invocation) throws Throwable {String methodName = invocation.getMethod().getName();Object target = invocation.getTarget();if (target instanceof Executor) {return handleExecutorInvocation(invocation, methodName);} else if (target instanceof StatementHandler) {return handleStatementHandlerInvocation(invocation);}return invocation.proceed();}}
2. SQL执行上下文管理
public class SqlExecutionContext {private long startTime;private String sql;private Object parameter;private MappedStatement mappedStatement;private List<ParameterMapping> parameterMappings;private Object result;private Throwable exception;// 记录执行开始public void start() {this.startTime = System.currentTimeMillis();}// 记录执行结束public void end() {this.duration = System.currentTimeMillis() - startTime;}// 获取格式化参数public String getFormattedParameters() {// 参数格式化逻辑}}
3. Executor方法处理
private Object handleExecutorInvocation(Invocation invocation, String methodName) throws Throwable {SqlExecutionContext context = new SqlExecutionContext();contextHolder.set(context);try {context.start();// 获取执行参数MappedStatement ms = (MappedStatement) invocation.getArgs()[0];Object parameter = invocation.getArgs()[1];context.setMappedStatement(ms);context.setParameter(parameter);context.setSql(ms.getBoundSql(parameter).getSql());context.setParameterMappings(ms.getBoundSql(parameter).getParameterMappings());// 输出Preparing日志if (logger.isDebugEnabled()) {logger.debug("==> Preparing: {}", context.getSql());}// 执行原始方法Object result = invocation.proceed();context.setResult(result);context.end();// 输出结果日志logExecutionResult(context, methodName, result);return result;} catch (Throwable t) {context.setException(t);context.end();logExecutionError(context, methodName, t);throw t;} finally {contextHolder.remove();}}
4. StatementHandler方法处理
private Object handleStatementHandlerInvocation(Invocation invocation) throws Throwable {// 执行原始prepare方法Statement statement = (Statement) invocation.proceed();// 获取BoundSqlStatementHandler handler = (StatementHandler) invocation.getTarget();BoundSql boundSql = handler.getBoundSql();// 输出Parameters日志if (logger.isDebugEnabled()) {String parameters = formatParameters(boundSql.getParameterObject(), boundSql.getParameterMappings());logger.debug("==> Parameters: {}", parameters);}return statement;}
四、参数格式化与安全处理
1. 参数格式化实现
private String formatParameters(Object parameterObject, List<ParameterMapping> parameterMappings) {if (parameterMappings == null || parameterMappings.isEmpty()) {return "";}StringBuilder sb = new StringBuilder();ParamNameResolver paramNameResolver = new ParamNameResolver(configuration, method);for (int i = 0; i < parameterMappings.size(); i++) {ParameterMapping mapping = parameterMappings.get(i);String property = mapping.getProperty();try {Object value = getParameterValue(parameterObject, property, paramNameResolver);String formattedValue = formatParameterValue(value, mapping);if (i > 0) {sb.append(", ");}sb.append(formattedValue);} catch (Exception e) {sb.append("<?>");}}return sb.toString();}
2. 安全参数处理
private String formatParameterValue(Object value, ParameterMapping mapping) {if (value == null) {return "null";}// 敏感信息脱敏if (isSensitiveParameter(mapping.getProperty())) {return maskSensitiveData(value);}// 类型特定格式化if (value instanceof String) {return "'" + escapeString((String) value) + "'";} else if (value instanceof Date) {return "'" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) value) + "'";} else if (value instanceof Number) {return value.toString();} else {return "'" + value.toString() + "'";}}
3. 敏感数据脱敏
private String maskSensitiveData(Object value) {if (value == null) {return "null";}String strValue = value.toString();// 手机号脱敏if (strValue.matches("1[3-9]\\d{9}")) {return strValue.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");}// 身份证号脱敏if (strValue.matches("\\d{17}[\\dXx]")) {return strValue.replaceAll("(\\d{6})\\d{8}(\\d{4})", "$1********$2");}// 默认脱敏规则if (strValue.length() > 4) {return strValue.substring(0, 2) + "****" + strValue.substring(strValue.length() - 2);}return "****";
}
五、完整的日志输出格式
1. 查询操作日志输出
private void logQueryResult(SqlExecutionContext context, Object result) {long duration = context.getDuration();String sql = context.getSql();if (result instanceof List) {int size = ((List<?>) result).size();logger.debug("<== Total: {} | Time: {}ms", size, duration);} else {logger.debug("<== Total: 1 | Time: {}ms", duration);}// 详细模式输出结果样本if (logger.isTraceEnabled()) {logDetailedResult(result);}
}
2. 更新操作日志输出
private void logUpdateResult(SqlExecutionContext context, int affectedRows) {long duration = context.getDuration();logger.debug("<== Updates: {} | Time: {}ms", affectedRows, duration);
}
3. 错误日志输出
private void logExecutionError(SqlExecutionContext context, String methodName, Throwable t) {String sql = context.getSql();long duration = context.getDuration();logger.error("SQL Execution Failed: {} | Time: {}ms", sql, duration, t);// 输出参数信息用于调试if (logger.isDebugEnabled()) {String parameters = formatParameters(context.getParameter(), context.getParameterMappings());logger.debug("==> Parameters: {}", parameters);}
}
六、高级特性实现
1. 执行时间监控与慢SQL告警
private void checkSlowQuery(SqlExecutionContext context) {long duration = context.getDuration();long slowQueryThreshold = getSlowQueryThreshold();if (duration > slowQueryThreshold) {String sql = context.getSql();logger.warn("Slow SQL detected: {}ms - {}", duration, sql);// 发送告警通知alertSlowQuery(sql, duration, context.getParameter());}
}
2. SQL执行统计
public class SqlStatistics {private static final ConcurrentHashMap<String, SqlStats> statsMap = new ConcurrentHashMap<>();public void recordSqlExecution(String sql, long duration, boolean success) {SqlStats stats = statsMap.computeIfAbsent(sql, k -> new SqlStats());stats.incrementExecutionCount();stats.recordDuration(duration);if (!success) {stats.incrementErrorCount();}}public void logStatistics() {if (logger.isInfoEnabled()) {logger.info("SQL Execution Statistics:");statsMap.forEach((sql, stats) -> {logger.info("SQL: {} - Executions: {}, AvgTime: {}ms, Errors: {}",abbreviateSql(sql), stats.getExecutionCount(),stats.getAverageDuration(), stats.getErrorCount());});}}
}
3. 动态日志级别控制
public class DynamicLogLevelManager {private volatile LogLevel currentLevel = LogLevel.DEBUG;public enum LogLevel {NONE, BASIC, PARAMS, PERFORMANCE, FULL}public void setLogLevel(LogLevel level) {this.currentLevel = level;}public boolean shouldLogSql() {return currentLevel != LogLevel.NONE;}public boolean shouldLogParameters() {return currentLevel == LogLevel.PARAMS || currentLevel == LogLevel.PERFORMANCE || currentLevel == LogLevel.FULL;}public boolean shouldLogPerformance() {return currentLevel == LogLevel.PERFORMANCE || currentLevel == LogLevel.FULL;}
}
七、生产环境配置建议
1. 日志配置文件示例
<configuration><!-- SQL日志单独配置 --><logger name="SQL_LOGGER" level="DEBUG" additivity="false"><appender-ref ref="SQL_FILE"/><appender-ref ref="CONSOLE"/></logger><!-- SQL文件输出 --><appender name="SQL_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><file>logs/sql.log</file><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>logs/sql.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>7</maxHistory></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %msg%n</pattern></encoder></appender><!-- 慢SQL单独记录 --><logger name="SLOW_SQL_LOGGER" level="WARN"><appender-ref ref="SLOW_SQL_FILE"/></logger>
</configuration>
2. 插件配置优化
<plugins><plugin interceptor="com.example.SqlLoggingPlugin"><property name="logLevel" value="PARAMS"/><property name="slowQueryThreshold" value="1000"/><property name="maskSensitiveData" value="true"/><property name="sensitivePatterns" value="password,idCard,mobile"/></plugin>
</plugins>
八、为什么分离输出Preparing和Parameters
1. 安全性考虑
-
避免敏感数据泄露:分离输出可以灵活控制参数日志的开启关闭
-
SQL注入防护:不直接输出拼接后的SQL,避免误导开发者使用字符串拼接
2. 调试便利性
-
参数清晰可见:每个参数值独立显示,便于识别null值或特殊值
-
类型信息保留:参数保持原始类型信息,便于类型相关问题的调试
3. 性能优化
-
减少字符串操作:避免不必要的SQL字符串拼接操作
-
条件性输出:可以独立控制SQL文本和参数值的输出级别
九、MyBatis标准日志输出格式
MyBatis官方推荐的日志输出格式通常包含三个部分:
==> Preparing: SELECT * FROM users WHERE id = ? ==> Parameters: 123(Integer)<== Total: 1
这种格式清晰地区分了SQL文本、参数信息和执行结果,成为了MyBatis生态中的事实标准。
十、总结
实现一个完整的MyBatis SQL日志插件需要综合考虑功能完整性、性能影响、安全性等多个方面。通过拦截Executor和StatementHandler的关键方法,我们可以获取到SQL执行的完整上下文信息,并输出专业级的日志信息。
在实现过程中,要特别注意敏感数据的保护,避免将密码、身份证号等敏感信息明文输出到日志中。同时,通过合理的架构设计,支持动态配置和扩展,使插件能够适应不同环境的需求。
一个优秀的SQL日志插件不仅是调试工具,更是系统监控和性能优化的重要基础设施。通过本文介绍的技术方案和最佳实践,您可以构建出满足生产环境要求的专业级MyBatis SQL日志解决方案。
💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!💖常来我家多看看,
📕我是程序员扣棣,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!