ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

让MyBatis-Plus控制台打印完整带参SQL的多种配置方案

让MyBatis-Plus控制台打印完整带参SQL的多种配置方案 1. 为什么你的控制台里永远只看到一堆问号先聊个真实场景。你用 MyBatis-Plus 做列表查询代码没有任何问题但控制台打出来的 SQL 长这样 Preparing: SELECT id,name,age,email FROM user WHERE id ? Parameters: 1(Long)肉眼看着是不是挺痛苦小数据量倒无所谓一旦 SQL 复杂一点、参数多几个你要想把这个 SQL 复制到 Navicat 里手动跑一遍就得上上下下比对哪个问号对应哪个参数。尤其是排查线上数据问题的时候这种半截子 SQL会让你多花好几倍时间。这个问题的根源其实很简单MyBatis-Plus 底层是 MyBatis而 MyBatis 对 SQL 的处理遵循的是 JDBC 的预编译规范——先用问号占位再通过PreparedStatement.setXxx()把参数一个个绑进去。这种机制在性能和防注入上没得黑但它在日志输出时天然就是SQL 和参数分家的控制台里印出来的自然就是占位符版本。我见过不少人以为这是 MyBatis-Plus 的 bug跑到 GitHub 上提 issue其实不是。MyBatis 默认的日志工厂会根据 classpath 里已有的日志框架自动选择输出方式而绝大多数 Spring Boot 项目的日志级别是 infoMyBatis 的 SQL 日志又恰好挂在 debug 级别下所以很多人连上面那两行都看不到更别提带参数的完整 SQL 了。这篇文章就把我平时在项目里用的几种让控制台打印完整带参数 SQL的方案全部摊开讲一遍包括它们各自的适用场景、配置方法、不生效的排查思路以及我在生产环境里踩过的坑。内容不算难但确实属于那种没配过就不知道哪里出错的典型问题。2. 最直接的正统方案改log-impl配置让 SQL 走标准输出2.1 配置就一行但你要知道它背后做了什么MyBatis-Plus 在application.yml里提供了一个配置项官方文档叫log-impl。它的作用是指定 MyBatis 的日志实现类。你只需要写成这样mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl如果是.properties后缀的配置文件对应写法是mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl配置完成后重启应用再跑一次刚才的查询控制台会变成这样 Preparing: SELECT id,name,age,email FROM user WHERE id ? Parameters: 1(Long) Columns: id, name, age, email Row: 1, 张三, 18, zhangsanexample.com Total: 1看到没有参数在线打印出来了查询结果里的字段名和每一行数据也都印出来了。排查问题的时候你直接把Preparing那一行 SQL 里的问号手动替换成 Parameters 里的实际值就能拿去数据库客户端里执行不用再猜来猜去。2.2 为什么选 StdOutImpl 而不是别的 Impllog-impl的值本质上是org.apache.ibatis.logging.Log接口的实现类MyBatis 自带了好几个StdOutImpl、Slf4jImpl、Log4jImpl、Log4j2Impl、NoLoggingImpl、Jdk14LoggingImpl、CommonsLoggingImpl。这里面最值得玩味的是StdOutImpl。StdOutImpl的实现非常简单粗暴它不走任何日志框架直接调用System.out.println()把 SQL 打到标准输出流。这就带来一个好处——它不受你项目的日志级别配置影响。哪怕你 logback 把根级别设成 error控制台照样能把 SQL 打出来因为标准输出和日志框架压根不是一条链路。这个特性在本地调试时非常香。你不需要关心项目里到底用的 Logback 还是 Log4j2也不需要去翻日志框架的配置文件一行配置搞定。我见过很多新手在log-impl和logging.level之间来回折腾搞了半天 SQL 还是不出来最后发现是 root 级别太高把 debug 日志过滤掉了。用StdOutImpl就没这个烦恼。但反过来它也有天然的短板既然走了System.out那就意味着你没法对这个 SQL 日志做统一归档、按天切分、按级别过滤这些操作它跟业务日志完全是两套体系。所以我的建议是StdOutImpl适合开发环境、测试环境日常调试用别把它当成生产环境的长期配置。生产环境想观察 SQL应该用后面介绍的方案。2.3 顺手聊聊NoLoggingImpl和Slf4jImpl跟StdOutImpl对应的还有两个实现类你可能会在其他项目里碰到。NoLoggingImpl顾名思义就是完全关闭 MyBatis 的 SQL 日志输出。有时候团队里有人嫌日志太多直接在配置里写了它结果所有人排查问题时都看不见 SQL。如果你发现项目里 SQL 日志消失得无影无踪先去log-impl里看一眼是不是被设成了NoLoggingImpl。Slf4jImpl则是走 SLF4J 门面输出配合 Spring Boot 默认的 Logback 使用。配置它之后SQL 日志的级别是 debug并且在 Logback 配置里可以通过 logger name 来精细控制例如只让某个 mapper 包输出 SQL。它不像StdOutImpl那样无视日志级别所以配置完还要记得把对应包的日志级别降到 debug或者用logging.level来覆盖。这一点是很多人踩坑的重灾区后面我会专门展开讲。3. 更省心的logging.level方案配合日志系统精细控制生产环境也能用3.1 原理和配置方式如果说log-impl: StdOutImpl是粗暴输出那logging.level方案就是精致管理。它的思路是不改变 MyBatis 的日志实现类让它保持默认的 SLF4J 适配而是要靠日志框架本身来决定 SQL 日志是否输出。具体做法是在application.yml里加一行logging: level: com.example.demo.mapper: debug这里com.example.demo.mapper换成你自己的 Mapper 接口所在的包路径或者直接指定某个 Mapper 类的全限定名例如com.example.demo.mapper.UserMapper: debug。配置完成后控制台输出的日志会带 Logger 名称长这样2025-01-15 10:23:45.678 DEBUG 12345 --- [nio-8080-exec-1] c.e.d.mapper.UserMapper.selectById : Preparing: SELECT id,name,age,email FROM user WHERE id ? 2025-01-15 10:23:45.679 DEBUG 12345 --- [nio-8080-exec-1] c.e.d.mapper.UserMapper.selectById : Parameters: 1(Long) 2025-01-15 10:23:45.679 DEBUG 12345 --- [nio-8080-exec-1] c.e.d.mapper.UserMapper.selectById : Total: 1有日志框架统一的时间戳、线程号、Logger 名称看起来是不是像正规军了这里有个关键前提你的log-impl不能是NoLoggingImpl。如果项目里有其他地方把它关了那logging.level配得再对也没用。比较稳妥的做法是把log-impl配成org.apache.ibatis.logging.slf4j.Slf4jImpl或者干脆不配log-impl让 MyBatis 自己从 classpath 里探测 SLF4J。3.2 生产环境怎么临时看一眼这种方案在生产环境最大的价值在于你可以通过调整日志级别临时打开某个 Mapper 的 SQL 日志不需要改代码、不需要重新发版。比如你使用的是 Spring Boot 的 actuator 和 Spring Cloud Config那完全可以利用/actuator/loggers这个端点在线修改某个 logger 的级别。没有 actuator 的话也可以登录服务器改 Logback 配置再 reload。看完了再调回 info整个过程对业务无感。我有一年在排查一个线上数据不一致的问题时就是靠这种方式把某个核心 Mapper 的日志级别临时调成了 debug跑了几分钟拿到完整 SQL 和参数确认了是时间字段的时区转换导致查询范围出错。问题定位完日志级别立刻调回 info不会给磁盘带来压力。3.3 和StdOutImpl的取舍简单总结一下这两者的取舍对比维度StdOutImpllogging.level 方案输出路径System.out 直接输出走 SLF4J落入日志文件日志级别不受日志框架限制受日志框架级别限制需 debug格式只有 MyBatis 自己的输出格式有统一时间戳、线程号、Logger 名生产可用性不建议开可临时开启但要注意量局部关闭做不到可按 Mapper 包/类精确开关配置复杂度一行搞定需要理解日志体系要是你懒、只想本地赶紧看到 SQL选StdOutImpl。要是你想在生产环境排查问题时安全地开日志就必须走logging.level这条路线。两条路不是互斥的本地开发用StdOutImpl运维环境把log-impl去掉或设成Slf4jImpl然后靠logging.level控制开关是我比较推荐的组合。4. 让 SQL 完整拼出来的两个高级姿势p6spy 与自定义拦截器4.1 p6spyJDBC 驱动层的代理打印整条可执行 SQL如果你觉得上面两种方式打出来的 SQL 还是分家的——Preparing一行、Parameters一行拼接起来麻烦——那你想要的效果其实是控制台直接输出一条完整的、可以直接复制的 SQL例如SELECT id,name,age,email FROM user WHERE id 1这时候就轮到 p6spy 出场了。p6spy 做的事情是在 JDBC 驱动上包了一层代理。MyBatis 最终执行 SQL 要通过 JDBC 驱动p6spy 就盯在这里等 MyBatis 把占位符和参数都传给真正的驱动之前它把参数一个个取出来自动替换掉问号拼成一条完整 SQL再通过自己配置的 appender 输出。所以它的优点很明确输出的是成品 SQL不是半成品。接入 p6spy 的步骤有三步。第一步在pom.xml里加依赖dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version /dependency第二步改数据源配置。原来的jdbc:mysql://localhost:3306/demo要改成jdbc:p6spy:mysql://localhost:3306/demo驱动类也要改成com.p6spy.engine.spy.P6SpyDriverspring: datasource: url: jdbc:p6spy:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8 driver-class-name: com.p6spy.engine.spy.P6SpyDriver注意数据库驱动本身的 jar 包还是要保留的p6spy 只是代理不是替代品。第三步在src/main/resources下新建spy.properties最简配置长这样appendercom.p6spy.engine.spy.appender.Slf4JLogger logMessageFormatcom.p6spy.engine.spy.appender.CustomLineFormat customLogMessageFormat%(currentTime) | took %( executionTime)ms | %(sqlSingleLine)%(sqlSingleLine)这个格式符会把 SQL 占位符替换成真实参数并且压缩成单行。executionTime是这次 SQL 的耗时排查慢 SQL 时非常好用。配置好再跑一次查询控制台会多出这么一行取决于你用的 appender2025-01-15 10:23:45.681 INFO 12345 --- [nio-8080-exec-1] p6spy : 2025-01-15 10:23:45 | took 1ms | SELECT id,name,age,email FROM user WHERE id 1有没有很惊喜整条 SQL 是拼好的复制就能直接用还能看到耗时。我现在本地开发时配的 SQL 日志就是 p6spy 的格式排查问题的效率高出一截。不过要注意p6spy 是有性能损耗的。它在代理层做参数替换和字符串拼接如果你用了很多批量插入日志量会非常夸张。我在一个导入场景里试过一次万级数据的批量插入p6spy 的日志直接让控制台滚动到没法看。生产环境如果真要开一定要配合exclude配置把某些高频的查询排除掉或者限定只在测试环境使用。4.2 自定义 MyBatis 拦截器嫌弃 p6spy 重那就自己拼有的项目不方便引入第三方依赖但你又想要完整的带参数 SQL这时候可以写一个 MyBatis 拦截器来实现。拦截器可以拦截Executor的query和update方法在方法执行前后从Invocation参数里拿到MappedStatement再通过BoundSql取出原始 SQL 和参数映射关系手动把参数拼进去。核心代码如下Intercepts({ Signature(type Executor.class, method query, args {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), Signature(type Executor.class, method update, args {MappedStatement.class, Object.class}) }) public class SqlLogInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; BoundSql boundSql mappedStatement.getBoundSql(parameter); String sql boundSql.getSql(); ListParameterMapping parameterMappings boundSql.getParameterMappings(); Configuration configuration mappedStatement.getConfiguration(); long start System.currentTimeMillis(); try { return invocation.proceed(); } finally { long end System.currentTimeMillis(); String fullSql buildSql(sql, parameterMappings, configuration, parameter); System.out.println( fullSql | cost: (end - start) ms); } } private String buildSql(String sql, ListParameterMapping parameterMappings, Configuration configuration, Object parameter) { // 遍历 parameterMappings从 parameter 里取出对应字段值 // 注意处理 String 加单引号、日期格式化、null 值等情况 // 用值替换 SQL 中的问号 // 可以参考 MetaObject 来反射获取参数值 return sql; // 此处仅为示意 } }我上面这段代码只给了骨架真正实现的时候有几个容易忽略的细节如果参数是nullSQL 里应该拼成NULL而不是乱拼字符串字符串类型要加单引号日期类型要格式化成数据库认识的格式分页插件、逻辑删除插件可能已经改了 SQL你拿到的是改写后的 SQL打印的时候要注意批量参数foreach时参数集合需要循环取出这是最麻烦的地方。说实话完整实现一个健壮的 SQL 拼装拦截器工作量不小要考虑的参数类型分支很多。所以除非团队确实有特殊要求否则我更推荐用现成的 p6spy 或者日志框架方案拦截器我一般只用来做简单的耗时统计SQL 拼接这种活交给专门的工具干。5. 配置半天不生效把这些坑排查一遍你就彻底明白了5.1 配置文件层级写错是最常见的翻车现场log-impl的正确层级是mybatis-plus.configuration.log-impl。但很多人会把它跟 MyBatis 原生的mybatis.configuration搞混写出来变成mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl你看上去没什么区别但log-impl项目在 MyBatis 原生的Configuration里根本不存在这个属性会被忽略。结果就是项目启动正常、查询正常但控制台死活不打印 SQL。还有的人会写成mybatis-plus.log-impl少了一层configuration同样不生效。排查方法很简单看 IDEA 里 yml 编辑页的自动提示。你输入mybatis-plus后如果弹出configuration子项说明路径是对的如果光标在log-impl上没有任何提示大概率是层级不对或拼写有误。5.2 日志级别太高Slf4jImpl 被拦在门外如果你用的是logging.level方案或者log-impl写的是Slf4jImpl那 SQL 日志能不能打出来完全取决于你配置的日志级别。Spring Boot 的根日志级别默认是 info而 MyBatis 的 SQL 日志是 debug 级别一边是 debug、一边是 info直接过滤掉。这时候正确的做法是给 Mapper 所在的包单独开 debug在第 3 节已经讲过而不是去改根日志级别。改成根级别为 debug 虽然也能看到 SQL但项目里所有框架的 debug 日志会一起涌出来控制台瞬间爆炸生产环境更是灾难。5.3 PostgreSQL 和 MySQL 的 PreparedStatement 日志效果并不一样如果你项目用的是 PostgreSQLMyBatis 的日志里参数可能不会显示成Parameters: 1(Long)因为 PG 的驱动本身支持%s类型的参数日志风格。MySQL 这边一般都会正常显示但如果你同时用了某些连接池的 Prepare 缓存比如 Druid 的PSCache日志输出可能被缓存优化掩盖。这种情况比较少见但排查时要知道这个方向别在配置上死磕。5.4 自定义了 SqlSessionFactory绕过了 MyBatis-Plus 的自动配置这个坑藏得很深。有些老项目或者组件化开发的项目会自己手动构造SqlSessionFactory比如在配置类里写了类似于Bean public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean factoryBean new MybatisSqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); return factoryBean.getObject(); }一旦存在自定义的SqlSessionFactoryBeanMyBatis-Plus 的自动配置就有可能被绕过log-impl自然也就失效了。这种情况下就算 yml 里配置写上天也没用。解法是在手动构建时显式设置 Configuration或者在工厂 Bean 的属性里补充对应配置。我在一个老项目里就碰到过这种情况Spring Boot 配置检查了无数遍就是没输出 SQL最后翻代码发现配置类里自己 new 了SqlSessionFactoryBean而且没设 configuration。加上之后问题瞬间解决。5.5 批量插入时 SQL 日志疯狂刷屏你怎么忍前文提过的批量插入这里单独拿出来说。MyBatis-Plus 的saveBatch底层是循环执行 SQL 还是拼接大 SQL取决于你用的版本和数据库方言。如果是循环执行那每一条都会单独打日志日志量直接爆棚如果是拼接大 SQL一条 SQL 几千个参数打出来的日志会非常长。这种场景下上面所有打印 SQL 的方案都会变成负担。我的做法是批量导入的 mapper 方法在方法内临时关闭日志输出或者干脆让批量操作走独立的 mapper 包这个包的日志级别保持 info不参与 SQL 输出。排查批量问题时再临时把级别改回来问题定位完之后记得关掉。5.6 一步步排查链路照着走一遍就能定位为了让你不迷路我把排查顺序整理成一条链路按这个顺序检查基本上不会漏确认项目里实际生效的配置文件是哪一个。多环境项目里application-dev.yml、application-prod.yml可能有不同的覆盖关系你改的文件未必是最终生效的那个。检查log-impl是不是被设置成了NoLoggingImpl如果是任何方案都白搭。看控制台启动日志里有没有 MyBatis-Plus 的 banner 和配置加载信息确认mybatis-plus配置确实被读取了。如果走logging.level方案确认 Mapper 包路径写对了且log-impl没有显式关闭 SLF4J。搜一下项目里有没有手动创建SqlSessionFactory的代码有的话看它有没有继承 MyBatis-Plus 的自动配置。还不行就在StdOutImpl或者 p6spy 的源码里打一个断点看是不是根本没走到日志输出那一步。6. 配置速查表把常用方案直接抄进你的项目最后给一张我自己常用的选型表你可以直接对号入座场景推荐配置备注本地快速调试mybatis-plus.configuration.log-implorg.apache.ibatis.logging.stdout.StdOutImpl一行搞定日志级别不用管测试环境生产临时排查logging.level.你的mapper包名debuglog-impl 保持Slf4jImpl或不配可以精确控制走统一日志体系需要打印拼装完整 SQL引入 p6spy改 dataSource url 和 driver配 spy.properties带耗时SQL 可直接复制执行批量操作日志筛选单独 mapper 包保持 info 级别避免日志刷屏按需开启完全不想打 SQLlog-implorg.apache.ibatis.logging.nologging.NoLoggingImpl生产环境裁减日志量时用如果你只是本地调试我最推荐的组合是StdOutImpl加上 IDEA 控制台的日志过滤。如果你要上生产临时排查老老实实走logging.level别用StdOutImpl的 System.out因为它没法纳入日志文件归档出问题你回头看日志啥都没有。最后再分享一个我在实际项目中比较顺手的小技巧本地开发环境配 p6spy 看完整 SQL 和耗时提交代码前把p6spy的依赖和配置从pom里的 profile 区分出来只在本地 profile 生效这样就不会污染测试环境和生产环境。用 Maven profile 把 p6spy 依赖标成dev专属其他环境直接不加载既方便又能避免线上日志风暴。
返回列表