资讯动态

SpringBoot集成P6spy:让MyBatis-Plus打印完整带参SQL

发布时间:2026/10/14 8:43:32 来源:尧图企业网站定制
说实话我第一次听人说“SpringBoot项目里为什么要装P6spy”的时候心里是有点不以为然的。MyBatis-Plus自带SQL日志打印控制台里每天刷出来的SQL一抓一大把看都看不过来还专门引一个第三方组件去“偷看SQL”这不是脱裤子放气吗直到后来有个下午我被一条批量插入的脏数据折磨到怀疑人生——日志里明明打印了二十多行INSERT可每一行的参数位都是问号我对着数据库里的记录逐条比对半个多小时愣是没定位到是哪条参数写岔了。从那以后我才明白能让你在日志里直接看到“完整带参数SQL”的工具和只给你看带问号模板SQL的配置完全是两个时代的东西。这篇博文就基于“SpringBoot MyBatis-Plus P6spy 环境搭建”这个实操主题展开。我会把P6spy是什么、为什么要用它、依赖怎么引、spy.properties配置怎么填、数据源怎么改以及我踩过的几个真实坑一次说清楚。适合正在用SpringBoot做后端开发、尤其是用MyBatis-Plus做持久层、又被日志里一堆问号困扰的同学参考。1. 为什么说MyBatis自带的日志解决不了参数问题1.1 我遇到的那个“全是问号”的下午先还原一下场景。某个模拟项目X里有一段批量更新逻辑循环里调用了MyBatis-Plus的updateById一共要更新三十几条记录。控制台日志开的是DEBUG级别MyBatis的SQL日志倒也打印了打印出来是这个样子 Preparing: UPDATE user SET name?, age?, email? WHERE id? Parameters: 张三(String), 18(Integer), zhangsanexample.com(String), 1(Integer)看上去信息很全对吧问题在于当你有三十多条这样的日志连在一起时肉眼根本分不清哪条update对应哪条id。你以为看到了参数但实际上你看到的是一串离散的值得自己脑内完成“第几条SQL对应第几个参数”的匹配。如果中间哪条参数拼错了排查起来就是灾难。更麻烦的是MyBatis-Plus的ServiceImpl里如果嵌了循环调用每一条SQL的Preparing和Parameters是分开打印的二者之间还经常被其他日志穿插。你看着看着就懵了这条Parameters到底是配给上面那行Preparing还是下面那行1.2 三种打SQL方案的对比后来我调研了一圈发现想在开发环境看到“完整带参数SQL”主流方案无非三种方案打印效果侵入性维护成本适合场景MyBatis自带的日志配置模板SQL与参数分离低低简单查询、少量日志某数据库连接池的SQL监控过滤器能看到部分完整SQL中中已经用了对应连接池的项目P6spy JDBC代理层完整SQL带真实参数、耗时、分类低低所有需要调试SQL的开发环境三种方案我都用过。MyBatis自带日志虽然零配置但参数分离这个硬伤绕不过去。连接池自带的SQL监控能力其实不弱但它往往还兼任着性能统计、防火墙之类的功能配置项多而且换连接池就要重新配置一套学习成本反而上去了。P6spy则是独立于连接池和ORM框架之外的一层它直接代理JDBC驱动你发给数据库的每一个SQL、每一组参数它都能截获然后拼成一条完整SQL丢给你。1.3 P6spy到底在JDBC层做了什么P6spy的运作机制一句话概括就是它把自己伪装成JDBC驱动你的应用跟它说话它记录完日志再转告真正的数据库驱动。可以这么理解你原来直接打电话给数据库P6spy是中间插了一个话务员。你的应用把“我要执行一条SQL参数是某某某”这个请求交给话务员话务员先把完整内容抄录到记事本日志上然后原封不动转达给数据库。数据库返回结果后话务员又把结果抄一遍才能传回给你。因为拦截点在JDBC层所以它对上层是透明的。MyBatis-Plus、Hibernate、普通JDBC甚至你自己手写的Connection操作全都能被记录到这就是它比只盯着MyBatis日志要强的地方。而且它不改变SQL语义只是在链路里加了一个“记录员”对代码零侵入。2. 依赖引入与版本选型2.1 Maven坐标和版本建议引入P6spy非常简单一个依赖就够dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version /dependency我目前用得最稳的是3.9.1这个版本。3.x版本的配置格式比较统一网上能搜到的资料大多也是基于3.x写的遇到问题好查。不推荐再往更早的2.x版本去用了老版本的spy.properties里有些配置项在新版里已经改名比如dateformat和databaseDialectDateFormat在不同版本里兼容情况就不太一样新手很容易被老教程带偏。2.2 版本兼容的两个常见注意点第一个注意点P6spy本身不依赖Spring的任何东西它就是个JDBC层的拦截工具所以不存在和SpringBoot版本强绑定这回事。理论上只要你的数据源走的是标准JDBC就能接上。我自己在SpringBoot 2.x和3.x的项目里都验证过。第二个注意点P6spy和MySQL驱动之间的兼容性重点看驱动类名的写法。如果你的项目用的是MySQL 8.x以上版本驱动类是com.mysql.cj.jdbc.Driver走com.p6spy.engine.spy.P6SpyDriver代理时不用额外处理时区参数也能工作但日志里打印的时间默认走的是JVM默认时区这是后面要重点配置的一项。具体怎么配下面会讲。3. spy.properties配置文件逐项拆解P6spy装完之后核心就在classpath下新增一个spy.properties文件。这个文件是P6spy能否按你的心意工作的关键。我第一次配置时图省事只丢进去一个最简配置结果日志格式又丑又乱过滤条件也完全不对等于白装。后来我把配置逐项撸了一遍才算真正用明白。3.1 输出端配置先看我项目中一份完整可用的配置文件# 输出到SLF4J日志门面跟随项目logback/log4j2的配置 appendercom.p6spy.engine.spy.appender.Slf4JLogger # 启用自定义日志格式 logMessageFormatcom.p6spy.engine.spy.appender.CustomLineFormat # 自定义格式模板 customLogMessageFormat%(currentTime) | took %(executionTime) ms | %(category) | %(sql) # SQL中的日期参数格式化 databaseDialectDateFormatyyyy-MM-dd HH:mm:ss # 不参与记录的SQL分类 excludecategoriesinfo,debug,result,resultset # 开启慢SQL检测 outagedetectiontrue outagedetectioninterval5这里最核心的就是appender。P6spy有很多输出端Slf4JLogger的意思是P6spy把截获到的日志抛给项目的SLF4J门面最后由logback或者log4j2来输出。这就保证了日志的输出格式、级别、归档策略都和你项目现有的日志体系保持一致不会出现“P6spy自己打印一套、项目输出另一套”的割裂感。3.2 日志格式定制CustomLineFormat配合customLogMessageFormat是进阶用法。默认的SingleLineFormat虽然也能看但信息密度不够。我定制成%(currentTime) | took %(executionTime) ms | %(category) | %(sql)这样每一条SQL日志出来基本长这样2025-01-12 15:24:33:112 | took 3 ms | statement | select * from user where id 1 and is_deleted 0currentTime是执行时刻executionTime是这个SQL的执行耗时单位毫秒category是SQL分类statement、commit、rollback等sql就是拼接好的完整SQL。这个格式我强烈建议直接抄因为在开发环境定位问题时“耗时”和“完整SQL”两项信息几乎能解决80%的排查需求。有一点要留意customLogMessageFormat里的各个占位符是大小写敏感的写成%(SQL)是没有用的必须是%(sql)。我第一次就栽在这上面配完之后日志格式完全没变查了半天才发现是占位符拼写问题。3.3 过滤分类和慢SQL检测excludecategoriesinfo,debug,result,resultset这一步也非常关键。P6spy默认还会记录一些结果集内容、连接建立等垃圾信息如果不排除控制台会疯狂刷屏。加上这四个分类之后基本只保留真正需要看的DML/DDL语句。慢SQL检测我建议开发环境直接开outagedetectioninterval5表示超过5秒的SQL都会单独被标记出来。这个参数在定位批量更新、报表查询这类耗时操作时非常有用一眼就能看出哪条SQL是性能瓶颈。配置里我还需要提一个可选项过滤掉你不关心的SQL。比如些定时任务会定期执行select 1探活或者有一些你不想看的低频语句。P6spy支持在配置里通过完全匹配的方式排除某些SQL内容不同版本写法略有差异但基本思路一样。我一般只在日志实在太多的时候才会加过滤平时宁可多看一点也不要漏掉关键SQL。4. SpringBoot数据源改造与启动验证4.1 驱动和URL的修改方式配置文件就位之后接下来是SpringBoot数据源改造。这一步看似简单但有过不少同学栽在“驱动类名”上。直接看完整的application.yml配置spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql://localhost:3306/test_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456对比原来的配置总共改了两处driver-class-name从com.mysql.cj.jdbc.Driver改成com.p6spy.engine.spy.P6SpyDriverurl从jdbc:mysql://...改成jdbc:p6spy:mysql://...很多新人只改了driver-class-nameURL忘了加p6spy前缀启动直接报找不到驱动类或者告诉你“no suitable driver”。这两处必须同步修改缺一不可。另外值得一提的是P6spy其实还提供了另一种更“隐蔽”的接入方式数据源URL保持不变用com.p6spy.engine.spy.P6SpyDriverWrapper作为驱动类。这种方式的好处是改动更小但实测时会发现部分连接池对Wrapper的支持不如直接改URL来得干净。我个人的建议是开发环境统一走“改driver 改URL前缀”的标准方案稳。4.2 启动后看到的正常日志长什么样配置完成后启动应用正常情况你会看到类似下面这样的日志输出2025-01-12 15:30:02:114 | took 18 ms | statement | select id,name,age,email from user where id1 2025-01-12 15:30:02:132 | took 2 ms | statement | insert into user (name, age, email) values (测试用户, 18, testexample.com)注意这里的SQL是你真正发给数据库的那条完整语句参数已经被P6spy替换进SQL字符串里了。我第一次看到这个效果时最直观的感受是再也不用在IDE的Debug窗口里手动给问号配参数了。之前用MyBatis-Plus的selectById或LambdaQueryWrapper写条件查询时看到日志里的SQL往往是一长串自动生成的字段列表。现在你能完整看到它实际执行了什么包括MyBatis-Plus自动填充的逻辑删除条件is_deleted0、自动填充的时间字段等全都能看得清清楚楚。这对排查“为什么查出来数据不对”这一类问题效率提升非常明显。5. 实际项目中最常踩的四个坑5.1 加了依赖却不生效这是最常见的情况。依赖加了配置也写了启动也不报错但控制台就是看不到P6spy的日志。排查思路其实很简单按以下顺序走一遍确认spy.properties确实在classpath根目录下编译后的target/classes里能看到它确认日志输出级别Slf4JLogger输出的日志级别默认是INFO如果你的项目将某个包或全局日志级别调成了WARN以上那P6spy的输出会被直接吞掉确认数据源确实走的是修改过的url和driver-class-name如果项目里手动new了DataSource对象没有走application.yml配置就不会生效第三个点最容易忽略。很多项目为了做多数据源或读写分离会手动构建DataSource比如用ConfigurationProperties或者自定义DataSourceConfig。这种情况下光改application.yml是不够的必须手动把P6spy的驱动和URL塞进去。5.2 驱动类名冲突如果项目里有多个数据源或者混用了多个数据库连接池可能遇到驱动类名冲突。最典型的报错是启动时报Unable to load class: com.mysql.cj.jdbc.Driver这个报错不一定真的是你的MySQL驱动丢了而是P6spy的P6SpyDriver内部在尝试加载真实驱动时找不到对应的驱动类。解决方式一般是检查依赖里是不是漏了mysql-connector-java或者是否因为依赖排除机制把MySQL驱动给挤掉了。另外还要提醒一点如果项目同时接了多套数据库一套是MySQL一套是PostgreSQL那你大概率为不同数据源准备不同的驱动包装方式。P6spy本身支持同时代理多种驱动但配置复杂度会成倍上升。我的建议是开发环境只为当前正在调试的那个数据源开启P6spy另一个数据源保持原驱动不动能用就行别追求“全部接管”。5.3 日志刷屏和重复记录开启P6spy之后如果项目里用了连接池的预编译缓存你会发现同一条SQL可能会打印很多次。这是因为每次从连接池中取连接执行时P6spy都会如实记录这次执行过程即使SQL一模一样。这不算bug属于正常现象。如果想少刷一点屏可以先通过excludecategories把resultset之类的多余信息关掉再通过配置调整过滤条件将定时任务里固定的探活SQL排除掉。我个人从来不建议在开发环境做太激进的过滤因为过滤规则写错可能导致你漏掉真正出问题的SQL。5.4 时间格式和时区问题日志里的时间默认走customLogMessageFormat里的%(currentTime)但不同P6spy版本对这个时间的格式化处理不太一样。如果你发现输出的时间格式是一长串数字时间戳就需要在spy.properties里显式配置databaseDialectDateFormat或者对应的日期格式项。运行时区问题则更多来自MySQL连接参数。如果url里没配serverTimezone而你的数据库时区跟JVM默认时区不一致那么日志里的时间看起来会跟数据库里实际的时间差好几个小时。这个坑排查起来比较隐蔽因为它不影响SQL执行也不会报错但会让你对时间字段的分析产生误会。6. 让P6spy日志变成开发效率工具的进阶玩法6.1 快速定位慢SQL和批量操作瓶颈开发阶段开启慢SQL检测之后每一条执行超过阈值的SQL都会在日志里单独暴露出来。配合executionTime字段哪怕没有触发阈值也能直观比对不同写法的性能差异。有一次我排查一个批量更新性能问题原来用的写法是循环里单条updateByIdP6spy日志显示每条update耗时2~3毫秒两百条下来总耗时约600毫秒。后来改成批量更新从日志上一眼就能看到每批语句的耗时显著下降。如果没有P6spy把每条SQL的执行时间这样直接摆出来这种对比就只能靠分析工具或者手动打点麻烦得多。6.2 和MyBatis-Plus逻辑删除、自动填充的配合用MyBatis-Plus的同学一定对自动填充和逻辑删除不陌生但往往对它们“实际生成的SQL长什么样”没有概念。比如配置了逻辑删除的实体执行deleteById时你以为发的是delete实际MySQL收到的却是update。通过P6spy日志你能清清楚楚看到update user set is_deleted1, update_time2025-01-12 15:30:02 where id1 and is_deleted0这个信息在排查数据被“删掉”但记录还在的场景时特别有用。自动填充的create_time、update_time也一样如果发现入库时间不对看P6spy打印的最终SQL就能立刻判断是填充逻辑的问题还是数据库本身的问题。6.3 临时性过滤与后续扩展如果你觉得日志实在太吵可以在spy.properties里做临时性调整把一些高频且确定无关的语句排除掉。但记得开发调试完成后把过滤去掉不然这些临时的配置很容易变成“历史遗留问题”后期维护的人看着一头雾水。进阶一点P6spy的Slf4JLogger输出天然支持logback的过滤器。你可以在logback配置里针对P6spy的logger单独设置级别或输出到独立文件实现“普通日志归普通日志SQL日志归SQL日志”互不干扰。我在一个复杂项目里就是这么干的排查问题时直接打开一个专门的SQL日志文件不用在满屏的业务日志里翻来翻去效率非常高。最后再说一个我自己的使用习惯。我会在本地开发环境的application-dev.yml里开启P6spy而测试环境和生产环境的配置文件不引入这个依赖从根源上避免性能损耗和日志量膨胀。刚开始打开P6spy时你可能觉得日志有点吵但一旦习惯了在控制台直接看到带真实参数的完整SQL再回头去面对一堆问号你一定会觉得开发效率差了一个档次。这个组件不算新但它解决的是每个后端开发每天都会遇到的真实痛点值得你在下一个SpringBoot项目里给它留一个位置。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑