ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+MyBatis-Plus项目SQL日志完整输出与持久化配置指南

2026/8/5 16:55:15 拓冰建站 浏览量
SpringBoot+MyBatis-Plus项目SQL日志完整输出与持久化配置指南 1. 项目背景与核心诉求在基于SpringBoot和MyBatis-Plus的后端开发中排查数据库操作问题是个高频场景。无论是线上偶发的数据不一致还是测试环境里某个查询结果不符合预期我们第一时间想到的就是“刚才执行的SQL到底是什么它带的参数又是什么” 如果只能去翻看控制台那转瞬即逝的输出或者更糟在生产环境根本没有输出那排查效率会大打折扣。把SQL日志和参数持久化到独立的日志文件里这几乎是一个成熟项目的标配。它不仅仅是调试的利器更是线上问题追溯、审计甚至性能分析结合慢SQL的重要依据。很多开发者知道需要在application.yml里配个logging.level但实际做下来经常会遇到日志文件里只有SQL没有参数、参数显示为?占位符、或者日志输出过于杂乱影响可读性等问题。今天我们就来彻底解决这个问题。目标很明确在SpringBoot MyBatis-Plus的项目中将完整可执行的SQL语句及其真实的参数值清晰、结构化地输出到我们指定的日志文件里而不是淹没在控制台的海量信息中。我会基于常见的日志框架组合Logback/SLF4J从配置到原理再到避坑细节手把手带你实现。2. 日志框架选型与基础配置解析在动手之前我们需要理清SpringBoot默认的日志生态。SpringBoot 2.x/3.x 默认使用的是SLF4J作为日志门面并集成了Logback作为默认的实现。我们的配置将主要围绕Logback展开。如果你使用的是Log4j2核心思路相通但配置文件语法不同。首先我们得明确MyBatis-Plus以及底层的MyBatis是通过哪个Logger来输出SQL的。MyBatis内部使用其内置的日志工厂来适配各种日志框架。当我们设置了logging.level时实际上是控制了对应Logger的日志级别。核心配置application.ymllogging: level: # 关键配置将MyBatis-Plus相关的Mapper接口所在包的日志级别设为DEBUG com.yourcompany.yourproject.mapper: DEBUG # 另一种更精确的方式直接指定MyBatis使用的JDBC Logger org.apache.ibatis: DEBUG # 如果你需要看到更底层的连接池、事务等信息可以开启以下通常不需要 # org.springframework.jdbc.core.JdbcTemplate: DEBUG # org.springframework.jdbc.core.StatementCreatorUtils: TRACE file: # 指定日志文件路径和名称。不配置此项日志默认只输出到控制台。 name: ./logs/app.log logback: rollingpolicy: # 配置滚动策略避免单个日志文件过大 max-file-size: 10MB max-history: 30这段配置做了两件事设置日志级别将我们Mapper接口所在包或MyBatis核心包的日志级别设为DEBUG。这是必须的因为SQL语句的执行信息在MyBatis中属于DEBUG级别。如果设为INFO你将看不到任何SQL输出。指定日志文件通过logging.file.name指定了日志输出的文件路径。这样所有日志包括我们将要输出的SQL都会写入到./logs/app.log这个文件中。然而仅仅这样配置你可能会发现日志文件里出现了SQL但参数是像 Parameters: 1(Integer), “test”(String)这样的形式或者更糟只有?。这并没有达到我们“完整可执行SQL”的终极目标。接下来我们需要更精细地控制日志的输出格式和内容。3. 定制Logback配置以实现清晰SQL输出默认的SpringBoot Logback配置可能不符合我们的需求比如我们希望将SQL日志单独输出到一个文件或者调整其格式。我们需要创建一个自定义的logback-spring.xml文件放在src/main/resources目录下。完整的 logback-spring.xml 配置示例?xml version1.0 encodingUTF-8? configuration scantrue scanPeriod60 seconds !-- 定义统一的时间格式和彩色编码控制台用 -- property nameCONSOLE_LOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/ property nameFILE_LOG_PATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n/ !-- 1. 控制台输出 (彩色适合开发环境) -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${CONSOLE_LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 2. 将所有DEBUG及以上级别的日志输出到主应用文件 -- appender nameFILE-APP classch.qos.logback.core.rolling.RollingFileAppender file./logs/app.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy !-- 按日期和大小滚动 -- fileNamePattern./logs/app.%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize10MB/maxFileSize maxHistory30/maxHistory totalSizeCap3GB/totalSizeCap /rollingPolicy encoder pattern${FILE_LOG_PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 3. 【关键】将SQL相关日志单独输出到一个文件 -- appender nameFILE-SQL classch.qos.logback.core.rolling.RollingFileAppender file./logs/sql.log/file !-- 过滤器只接受特定Logger的日志 -- filter classch.qos.logback.classic.filter.ThresholdFilter levelDEBUG/level /filter rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern./logs/sql.%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize10MB/maxFileSize maxHistory7/maxHistory !-- SQL日志通常保留更短时间 -- totalSizeCap1GB/totalSizeCap /rollingPolicy encoder !-- 简化格式专注于SQL内容本身 -- pattern%d{yyyy-MM-dd HH:mm:ss.SSS} | %msg%n/pattern charsetUTF-8/charset /encoder /appender !-- Logger配置 -- logger namecom.yourcompany.yourproject.mapper levelDEBUG additivityfalse !-- additivityfalse 表示此logger的日志不再向上传递至root避免重复打印 -- appender-ref refFILE-SQL/ /logger !-- 也可以直接控制MyBatis的JDBC Logger -- logger nameorg.apache.ibatis levelDEBUG additivityfalse appender-ref refFILE-SQL/ /logger logger namejdbc.sqlonly levelDEBUG additivityfalse appender-ref refFILE-SQL/ /logger logger namejdbc.sqltiming levelINFO additivityfalse/ !-- 关闭其他过于冗长的JDBC日志 -- logger namejdbc.audit levelOFF/ logger namejdbc.resultset levelOFF/ logger namejdbc.resultsettable levelOFF/ logger namejdbc.connection levelOFF/ !-- 根Logger控制其他所有日志的输出 -- root levelINFO appender-ref refCONSOLE/ appender-ref refFILE-APP/ /root !-- 开发环境Profile在控制台也打印SQL -- springProfile namedev logger namecom.yourcompany.yourproject.mapper levelDEBUG additivityfalse appender-ref refCONSOLE/ appender-ref refFILE-SQL/ /logger /springProfile !-- 生产环境Profile可能只记录WARN以上级别且SQL日志单独收集 -- springProfile nameprod root levelWARN appender-ref refFILE-APP/ /root logger namecom.yourcompany.yourproject.mapper levelDEBUG additivityfalse appender-ref refFILE-SQL/ /logger /springProfile /configuration这个配置的核心在于FILE-SQLAppender我们创建了一个专门用于记录SQL的滚动文件Appender。通过filter和特定的logger配置将Mapper或MyBatis相关的日志定向到这里。additivityfalse这个属性至关重要。它阻止了日志事件向上传递到根Logger (root)。如果不设置为false那么SQL日志既会被FILE-SQL记录也会被根Logger配置的FILE-APP和CONSOLE记录导致日志文件中出现重复条目。springProfile利用Spring的Profile功能我们可以实现环境差异化的配置。在dev环境下SQL同时在控制台和sql.log文件中输出方便调试在prod环境下SQL只输出到sql.log文件控制台保持洁净。4. 解决参数占位符与获取完整可执行SQL即使配置了正确的日志级别和文件默认的MyBatis日志输出格式可能仍然不是最理想的。它通常将SQL语句和参数分开打印例如DEBUG 12345 --- [nio-8080-exec-1] c.y.p.m.UserMapper.selectById : Preparing: SELECT id, name, age FROM user WHERE id? DEBUG 12345 --- [nio-8080-exec-1] c.y.p.m.UserMapper.selectById : Parameters: 1(Integer) DEBUG 12345 --- [nio-8080-exec-1] c.y.p.m.UserMapper.selectById : Total: 1这对于阅读来说不够直观我们更希望看到的是拼接好参数的完整SQL类似于DEBUG 12345 --- [nio-8080-exec-1] c.y.p.m.UserMapper.selectById : Executing SQL: SELECT id, name, age FROM user WHERE id1方案一使用MyBatis-Plus的SqlInjector与自定义插件推荐MyBatis-Plus提供了强大的插件机制。我们可以通过一个简单的插件在SQL执行前后拦截并打印出我们想要的格式。创建自定义SQL日志打印插件import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.mapping.MappedStatement; import org.apache.ibatis.plugin.*; import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.SystemMetaObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import java.sql.Connection; import java.util.Properties; Intercepts({ Signature(type StatementHandler.class, method prepare, args {Connection.class, Integer.class}) }) Component public class SqlLoggerInterceptor implements Interceptor { private static final Logger log LoggerFactory.getLogger(SQL_LOGGER); // 可以使用特定Logger Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler (StatementHandler) invocation.getTarget(); MetaObject metaObject SystemMetaObject.forObject(statementHandler); // 分离代理对象链获取真实的StatementHandler while (metaObject.hasGetter(h)) { Object h metaObject.getValue(h); metaObject SystemMetaObject.forObject(h); } while (metaObject.hasGetter(target)) { Object target metaObject.getValue(target); metaObject SystemMetaObject.forObject(target); } MappedStatement mappedStatement (MappedStatement) metaObject.getValue(delegate.mappedStatement); String mapperId mappedStatement.getId(); BoundSql boundSql statementHandler.getBoundSql(); String rawSql boundSql.getSql(); Object parameterObject boundSql.getParameterObject(); // 获取并处理参数这里简化处理实际可能需要递归处理复杂对象 String formattedSql formatSql(rawSql, parameterObject, boundSql); // 使用我们指定的Logger和格式进行输出 log.debug(\n SQL LOG \nMapper ID: {}\nExecuting SQL: {}\n END \n, mapperId, formattedSql); return invocation.proceed(); } /** * 一个简单的SQL参数格式化示例生产环境建议使用更健壮的库如Apache Commons Lang */ private String formatSql(String sql, Object parameterObject, BoundSql boundSql) { if (parameterObject null) { return sql; } // 这里是一个极其简单的替换仅用于演示。实际参数映射非常复杂。 // 更安全的做法是直接使用MyBatis提供的ParameterHandler和TypeHandler来获取真实值。 // 或者可以直接打印 rawSql 和 boundSql.getParameterMappings()/boundSql.getParameterObject() // 对于简单的数字、字符串参数以下逻辑可能工作。 String formatted sql; for (Object paramValue : boundSql.getParameterObject() instanceof Map ? ((Map?, ?) boundSql.getParameterObject()).values() : new Object[]{boundSql.getParameterObject()}) { if (paramValue ! null) { formatted formatted.replaceFirst(\\?, paramValue.toString() ); } } return formatted; } Override public Object plugin(Object target) { return Plugin.wrap(target, this); } Override public void setProperties(Properties properties) { } }将插件注册到MyBatis-Plus的拦截器链中import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor(SqlLoggerInterceptor sqlLoggerInterceptor) { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 添加分页插件如果需要 interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); // 添加我们自定义的SQL日志插件 // 注意需要将自定义的Interceptor适配为InnerInterceptor这里为了演示假设SqlLoggerInterceptor实现了InnerInterceptor接口。 // 更常见的做法是自定义插件直接通过Bean注入MyBatis会自动扫描Intercepts注解。 // 因此通常不需要在这里添加只需要确保SqlLoggerInterceptor被Spring管理即可。 return interceptor; } // 确保自定义拦截器被Spring管理如上文的SqlLoggerInterceptor已标注Component }重要提示上面的formatSql方法是一个非常简陋的示例仅用于演示思路。真实生产环境中SQL参数替换涉及复杂的类型如日期、Clob、Blob、参数映射#{}和${}的区别、以及集合类型如IN语句。强烈不建议自己手写复杂的替换逻辑容易出错且不安全。更推荐下面的方案二。方案二使用P6Spy第三方JDBC驱动代理P6Spy是一个开源的JDBC驱动代理框架它可以拦截并记录所有通过JDBC发出的SQL语句及其参数并格式化成完整的可执行SQL。这是获取“完整SQL”最直接、最可靠的方式之一。添加依赖以Maven为例dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version !-- 请使用最新版本 -- /dependency修改数据源配置将你原来的JDBC URL中的驱动类和URL进行修改。原配置如使用MySQLspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/your_db?useSSLfalseserverTimezoneUTC修改为P6Spy配置spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver # 驱动类改为P6Spy的 url: jdbc:p6spy:mysql://localhost:3306/your_db?useSSLfalseserverTimezoneUTC # URL前加上 jdbc:p6spy:创建P6Spy配置文件(spy.properties放在src/main/resources下)# 指定真实JDBC驱动的完整类名 modulelistcom.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory # 自定义日志打印 logMessageFormatcom.baomidou.mybatisplus.extension.p6spy.P6SpyLogger # 使用日志系统记录sql appendercom.p6spy.engine.spy.appender.Slf4JLogger # 是否开启慢SQL记录 outagedetectiontrue # 慢SQL记录标准单位秒 outagedetectioninterval2 # 开启自动刷新日志文件 autoflushtrueMyBatis-Plus对P6Spy的增强MyBatis-Plus提供了一个更美观的日志格式类P6SpyLogger已在上述配置中指定。它会将SQL输出为如下格式非常清晰Consume Time15 ms 2023-10-27 14:33:25 Execute SQLSELECT id, name FROM user WHERE id 1通过P6Spy你可以非常方便地获得拼接好参数的完整SQL并且它支持多种日志输出方式文件、Slf4J等。缺点是它作为一层代理对性能有微小的开销并且在极少数情况下可能与某些连接池或监控工具冲突。5. 生产环境下的优化与注意事项将SQL日志输出到文件后在生产环境中我们需要考虑更多运维相关的问题。1. 日志级别动态调整在生产环境我们可能默认只记录WARN或ERROR级别的日志以节省IO。但当出现数据问题需要排查时又需要临时开启DEBUG级别的SQL日志。我们可以借助Spring Boot Actuator的Loggers端点来实现动态调整。添加依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-actuator/artifactId /dependency在application.yml中暴露端点注意安全应配合权限管理management: endpoints: web: exposure: include: loggers,health,info通过HTTPPOST请求动态修改日志级别curl -X POST -H Content-Type: application/json -d {configuredLevel:DEBUG} http://your-server:port/actuator/loggers/com.yourcompany.yourproject.mapper这样可以在不重启应用的情况下临时开启SQL日志问题排查完毕后再将其改回INFO或WARN。2. 日志文件管理与切割我们之前的logback-spring.xml已经配置了按日期和大小滚动SizeAndTimeBasedRollingPolicy。在生产环境还需要关注磁盘空间设置合理的maxHistory保留天数和totalSizeCap总大小上限并配合监控告警。日志清理可以考虑使用Linux的logrotate工具或Kubernetes的Sidecar容器进行更复杂的日志生命周期管理。3. SQL日志的安全与脱敏将完整的SQL特别是参数记录到日志文件存在安全风险例如可能暴露用户密码、手机号等敏感信息如果这些信息作为查询条件。必须在输出前进行脱敏处理。在自定义拦截器中脱敏在上述的SqlLoggerInterceptor的formatSql方法中在拼接SQL字符串前对特定参数进行脱敏。这需要你定义一套规则例如识别出参数名包含password、phone等字段时将其值替换为***。使用P6Spy的过滤器P6Spy也支持自定义过滤器com.p6spy.engine.spy.appender.CustomLineFormat可以在日志行输出前进行字符串替换和脱敏。架构层面最根本的应避免将明文密码等敏感信息作为查询条件。对于日志的访问权限也要严格控制。4. 性能考量频繁的DEBUG级别日志记录尤其是IO操作写文件会对应用性能产生影响。异步日志考虑使用Logback的异步Appender (AsyncAppender) 来包装FILE-SQLAppender。这样日志事件会被放入一个队列由单独的线程负责写入磁盘避免阻塞主业务线程。appender nameASYNC-SQL classch.qos.logback.classic.AsyncAppender discardingThreshold0/discardingThreshold !-- 队列满时是否丢弃低于某级别的日志0为不丢弃 -- queueSize256/queueSize !-- 队列大小 -- includeCallerDatafalse/includeCallerData !-- 是否包含调用者数据获取它开销大 -- appender-ref refFILE-SQL/ /appender然后在Logger中引用ASYNC-SQL。采样记录在极高并发场景下可以考虑只采样记录SQL日志例如每100条记录1条但这会丢失部分信息需权衡。5. 与监控系统集成单独的日志文件不利于集中分析和告警。可以考虑使用ELK Stack或Graylog通过Filebeat或Logstash收集sql.log文件发送到Elasticsearch再利用Kibana或Graylog进行可视化查询、分析和设置告警规则例如出现特定错误SQL或慢SQL时触发告警。使用APM工具如SkyWalking、Pinpoint等它们通常能提供更强大的链路追踪和SQL分析功能包括SQL执行时间、调用链关联等比单纯的日志更强大。6. 常见问题排查与解决思路在实际配置过程中你可能会遇到以下问题问题1配置了logging.level但日志文件里还是没有SQL。检查点确认配置的包路径是否正确。最准确的方式是查看应用启动时执行SQL的Mapper接口的完整类名。确认日志级别确实是DEBUG。Spring Boot的配置优先级命令行参数 application-{profile}.ymlapplication.yml。检查是否有其他配置覆盖了你的设置。检查logback-spring.xml是否被正确加载。可以在启动日志中搜索Loaded configuration from classpath:logback-spring.xml。如果没有可能是文件名不对或位置不对。检查自定义的Logger配置是否设置了additivityfalse并且没有同时被根Logger的级别过滤掉。问题2SQL日志输出到了控制台但没有输出到指定的文件。检查点确认logback-spring.xml中FILE-SQL这个Appender的file路径应用有写入权限。确认对应的Logger如com.yourcompany...mapper确实引用了FILE-SQL这个Appender (appender-ref refFILE-SQL/)。检查是否有其他日志框架冲突。确保项目中排除了Spring Boot默认的spring-boot-starter-logging如果使用Log4j2或者没有引入多个日志实现jar包。问题3使用P6Spy后应用启动报错或连接数据库失败。检查点检查P6Spy版本与你的JDK、Spring Boot版本是否兼容。检查spy.properties配置文件是否正确特别是modulelist和driverlist如果使用的配置。检查数据库连接池配置如HikariCP。有时需要在P6Spy的URL中传递一些原始驱动需要的参数或者需要调整连接池的驱动类识别。查看更详细的P6Spy启动日志它通常会输出在应用日志的开头部分。问题4日志文件过大增长过快。解决思路优化logback-spring.xml中的滚动策略减小maxFileSize减少maxHistory。确保只在必要的时候开启DEBUG级别的SQL日志。生产环境默认应为INFO或WARN通过Actuator动态开启。检查是否有循环调用或N1查询问题导致产生了大量重复或低效的SQL这本身也是性能问题需要优化。问题5脱敏规则复杂难以维护。解决思路考虑使用成熟的脱敏工具库如阿里云的fastjson配合JSONField的serialize/deserialize属性或jackson的注解在序列化到日志之前处理。但这通常需要将SQL参数对象进行序列化。将脱敏逻辑抽象成独立的服务或规则引擎通过配置文件来管理哪些字段需要脱敏以及脱敏规则提高可维护性。从源头避免审视业务设计尽量减少敏感信息在查询条件中的使用。通过以上六个部分的详细拆解从基础配置、高级定制、生产优化到问题排查你应该能够在你SpringBoot MyBatis-Plus的项目中游刃有余地配置和管理SQL日志了。记住清晰的日志是快速定位问题的基石花点时间搭建好这套基础设施会在未来的开发运维中节省大量时间。