有一次一个测试同事跑过来问我说某个查询接口在测试环境一直查不出数据但同样的条件在开发环境就能查出来。我第一反应不是去看业务代码而是让他把后端日志里的SQL语句发给我看。结果那头半天没动静——项目里明明挂了MyBatis-PlusSQL日志却根本没打开。这其实是个特别常见的现象很多人以为MyBatis-Plus默认就会打印SQL实际上它默认是关掉的。更麻烦的是网上搜“mybatisPlus打印sql配置”能找到一堆答案有让配log-impl的有让配logging.level的还有给插件推荐的结果配完有的生效、有的不生效甚至同一套配置换个项目就失灵了。这一篇就专门把这件事讲透覆盖三种主流方案的原理、适用场景、完整配置和坑点适合刚上手Spring Boot MyBatis-Plus的人照着抄也适合那些配过但没搞懂“为什么这个配置能用、那个配置没反应”的人。1. 配置了半天看不到SQL先弄懂MyBatis的日志门控1.1 两级门控SQL交给谁、谁愿意喊出来先说结论MyBatis-Plus打印SQL这件事本质上不是MyBatis-Plus的功能而是MyBatis框架的日志机制。MyBatis把SQL执行信息通过LogFactory交给当前项目里可用的日志实现MyBatis-Plus只是在此基础上多给了几个配置入口。所以如果你不了解这套日志机制配置很容易配了个“寂寞”。这套机制可以理解成两级门控第一级决定SQL日志“交给谁”。MyBatis启动时会自动探测项目里可用的日志框架按顺序找 Slf4j、Log4j2、Log4j、JdkLog、StdOut 等找到哪个用哪个。如果你显式指定了log-impl就相当于跳过自动探测直接指定一个实现类。第二级决定这个日志实现“喊不喊得出来”。即便SQL日志已经交给了某个日志框架它能不能输出到控制台还取决于对应的logger有没有达到输出阈值。比如你用的是Slf4j LogbackMyBatis调用的是logger.debug()方法那这条日志能不能打出来就取决于这个logger的级别是否允许debug级别通过。很多配置失效的根源都在第二级——日志已经交给Slf4j了但mapper包名下没有任何logging.level配置默认级别继承自root通常是infodebug级别的SQL日志就被拦掉了。1.2 MyBatis-Plus的log-impl到底做了什么MyBatis-Plus在配置文件中提供了一个mybatis-plus.configuration.log-impl参数。它的本质作用是覆盖MyBatis的LogFactory探测逻辑指定使用哪个org.apache.ibatis.logging.Log实现类。常见的可选实现类有实现类效果org.apache.ibatis.logging.stdout.StdOutImpl直接往控制台System.out打印不经过任何日志框架org.apache.ibatis.logging.slf4j.Slf4jImpl通过Slf4j输出受日志级别控制org.apache.ibatis.logging.log4j2.Log4j2Impl通过Log4j2输出org.apache.ibatis.logging.nologging.NoLoggingImpl关闭SQL日志输出如果你设置了StdOutImpl那么SQL日志会以 Preparing:这样的形式直接打到控制台不进入日志文件也不受日志格式配置影响。如果你设置了Slf4jImpl那么SQL依然是以debug级别打到Slf4j日志格式、文件输出、级别控制都走你配置的日志框架。这解释了为什么有人配了log-impl: StdOutImpl之后马上就看到SQL非常粗暴有效也解释了为什么有人配了log-impl: Slf4jImpl之后什么都看不到——因为Slf4j那一级门控没有打开mapper包名的logging.level没配成debug。1.3 SQL日志不输出时的排查决策顺序如果你现在项目里正好看不到SQL不用慌按下面这个顺序排查基本能定位先确认项目里是否有日志框架。纯Spring Boot项目默认带Logback一般都有。如果你用了无日志依赖的轻量封装那么Slf4jImpl可能根本找不到实现。确认配置的是哪条路。如果配了log-impl: StdOutImpl直接看控制台System.out输出别去看日志文件如果配了Slf4jImpl去查日志框架配置和logger级别。确认mapper接口包路径是否写对。配logging.level时写的是Mapper接口所在的包路径不是Mapper XML文件路径更不是实体类包路径。一个常见低级错误是把com.example.demo.mapper写成了com.example.demo.entity那当然打不出来。检查log-impl是否被写成了全限定类名但类名拼错。拼错一个字母不会启动报错但日志实现会回退到默认探测极难发现。有一次我在一个老项目里排查了半天最后发现是同事在配置文件里把stdout.StdOutImpl写成了stdout.StdoutImpl——大小写错了。类不存在MyBatis静默回退日志照旧不打这种问题不看源码根本不会往这儿想。2. 三种主流打印方式适用场景与完整配置2.1 方式一logging.level配合Slf4j走日志框架这是我最推荐的一种方式。它的好处是SQL日志会进入统一的日志体系可以单独设置输出级别、输出文件、日志格式也可以和生产环境其他日志统一归档。核心配置就两行logging: level: com.example.demo.mapper: debug其中com.example.demo.mapper换成你自己的Mapper接口包路径。如果项目里同时存在多模块、多包可以配置多个logging: level: com.example.demo.mapper: debug com.example.demo.other.mapper: debug如果想控制得更细可以在logback配置文件中单独指定logger并且关闭继承只让SQL日志进指定文件。下面是一个独立的logback配置片段把mapper包下的所有SQL日志单独写到一个 rolling 文件里appender nameSQL_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/sql.log/file rollingPolicy classch.qos.logback.core.rolling.TimeBasedRollingPolicy fileNamePatternlogs/sql.%d{yyyy-MM-dd}.log/fileNamePattern maxHistory30/maxHistory /rollingPolicy encoder pattern%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n/pattern /encoder /appender logger namecom.example.demo.mapper levelDEBUG additivityfalse appender-ref refSQL_FILE/ /logger注意additivityfalse这一行如果不设成falseSQL日志会同时往root logger里输出一遍控制台、主日志文件、SQL文件各打一份重复且难查。这种方式下MyBatis-Plus的配置文件中不需要额外设置log-impl让MyBatis自动探测到Slf4j即可。如果项目里确实需要显式指定可以写mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl但多数情况下没必要自动探测就够了。2.2 方式二log-impl StdOutImpl开发环境最快见效如果你是本地开发调试图省事可以直接用StdOutImpl。它不经过日志框架直接把SQL打印到System.out也就是IDE的Console窗口。mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl配置完重启应用执行一条SQL控制台会输出这样的内容 Preparing: SELECT id,name,age,email FROM user WHERE id? Parameters: 1(Long) Columns: id, name, age, email Row: 1, 张三, zhangsanexample.com Total: 1这种方式有几个明确的局限不走日志框架所以日志格式、文件归档、级别过滤全部失效。输出会混在System.out里和业务代码里的System.out.println挤在一起不容易筛选。生产环境基本没人敢这么干因为没法控制开关日志量大还会把敏感字段值打出来。所以我的建议是StdOutImpl只适合在本地开发的时候用而且用完最好就改回去别顺手提测。线上如果要用SQL日志请用下一节的方式三或者配合Actuator动态开关后面会讲。2.3 方式三P6Spy打印带真实参数的完整SQL前面两种方式打印出来的SQL参数值和SQL语句是分开的一是Preparing行的占位符语句一是Parameters行的参数列表。遇到中文模糊查询、时间区间查询的时候把两者拼起来才能看懂。但人眼拼多了总会有对不上的时候如果追求“拿来就能直接执行”的SQL就需要P6Spy。P6Spy是一个JDBC驱动的代理层它拦截真实数据库驱动执行前的SQL把预编译语句和参数值拼成一条可以直接执行的SQL再打印出来。使用方法分三步第一步引入依赖dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version /dependency第二步修改数据源配置让数据源走P6Spy驱动spring: datasource: driver-class-name: com.p6spy.engine.spy.P6SpyDriver url: jdbc:p6spy:mysql://localhost:3306/demo如果是Druid或者其他连接池记得把连接池配置里的驱动类名也一起改过来只改Spring的driver-class-name不一定够。第三步在资源目录下新建spy.propertiesdrivercom.mysql.cj.jdbc.Driver driverlistcom.mysql.cj.jdbc.Driver appendercom.p6spy.engine.logging.appender.StdoutLogger logMessageFormatcom.p6spy.engine.spy.appender.MultiLineFormat executionThreshold1000driverlist里写你真实的JDBC驱动类名。executionThreshold是慢SQL阈值单位毫秒设为1000表示超过1秒的SQL会额外标记出来。P6Spy打印的SQL长这样143 ms|1|connection 0|SELECT id,name,age FROM user WHERE id1|SELECT id,name,age FROM user WHERE id1可以看到参数已经被拼进SQL语句里了直接拷到数据库客户端执行即可。这对于分析业务SQL、复现慢SQL非常方便。2.4 三种方式的取舍对比很多人在博客搜到三种方案后就挨个试不如一开始就按需求选好。我做了一张对比表你可以直接拿去用对比维度logging.level Slf4jlog-impl StdOutImplP6Spy是否容易配置简单最简单中等要动数据源SQL是否带真实参数分开打印分开打印拼接后完整打印日志进日志框架是否可配置默认进控制台支持日志文件归档是否可配置生产环境可用性可临时开不建议不建议持续开启对SQL执行性能影响极小极小有一定额外开销是否能看到慢SQL统计否否是executionThreshold依赖优先级P6Spy的拦截发生在MyBatis之下、JDBC驱动之上所以它打印的SQL是最终真正交给数据库执行的那一条。而logging.level和log-impl打印的是MyBatis组装出来的预编译语句。两者在某些边界情况下会不一致比如MyBatis-Plus的分页插件会在发出SQL前自动拼接LIMIT你从Preparing里一般看不到拼接细节但P6Spy里能看到最终带LIMIT的完整SQL。3. 打印出来的SQL到底该怎么读3.1 Preparing与Parameters的对应关系用logging.level或StdOutImpl打印出来的SQL经常是两行配合着看 Preparing: SELECT id,name,age FROM user WHERE status ? AND name LIKE ? Parameters: 1(Integer), %张%(String)第一行是预编译后的SQL模板第二行是执行时绑定的参数列表参数顺序和SQL里的?顺序一一对应。很多人刚接触时看不懂1(Integer)这种写法其实就是在告诉你这个参数的类型是Integer值是1。如果你只拷贝Preparing那一行去数据库客户端执行会报参数数量不对必须手动把?替换成第二行的参数值。MyBatis-Plus的LambdaQueryWrapper生成的SQL有一个好处就是条件字段是实体字段映射过来的打印出来的SQL可以直接看到实际执行的字段名。比如你写user::getName但在数据库里字段名是name打印出来的SQL会显示name一眼就能发现字段映射是否正常。我在实际维护一个老项目时就靠这个特性抓过好几次字段映射错误——代码里Lambda写错了字段程序不报错但SQL条件明显不对看日志立刻现形。3.2 为什么有些SQL看不到预编译语句行有些人配好之后执行saveBatch批量插入时发现日志只输出一行Preparing后面没有对应的参数行或者SQL数量明显比插入的条数少。这不是配置有问题而是MyBatis的批量执行特性导致的。MyBatis-Plus的saveBatch走的是ExecutorType.BATCH模式多条插入语句会合并到同一个PreparedStatement上批量执行。MyBatis的日志机制在这种情况下不会逐条打印参数绑定信息所以你看到的是被合并后的少数几条日志。如果此刻你正在排查批量插入的SQL为什么慢建议改用P6Spy看最终发送到数据库侧的语句得到的信息更接近真实执行情况。3.3 从日志篇幅判断SQL是否异常除了看SQL内容本身日志的行数和耗时也能反映问题。正常情况下一次查询会输出Preparing、Parameters、Columns、Row、Total这几行。其中Columns和Row是结果集相关的信息如果查询结果特别大Row会非常多日志会被刷屏。如果一个SELECT连Columns都没有那就是执行出错了得去异常堆栈里找原因。另外日志里的分隔线和 Total: 0这种内容很多人会紧张以为是配置有问题。其实Total是查询返回的记录条数为0只是说明没有匹配数据不代表SQL执行失败。我刚带团队的时候有个新人看到Total: 0就怀疑是SQL日志打漏了反复调试了很久最后才发现是数据真的不存在。这种解读经验写在文档里没人看踩过一次就记住了。4. 坑点复盘重复配置、多数据源与线上误开4.1 log-impl和logging.level同时配置时的冲突很多人为了保险既配置了log-impl: StdOutImpl又配置了logging.level.xxx.mapper: debug结果控制台出现双份SQL日志。这是因为StdOutImpl不受日志级别控制它直接打印到System.out同时如果你配了logging.level项目的日志框架也会输出一份Slf4j的SQL日志。这属于配置冗余造成的混乱。正确的做法是“二选一”走logging.level方式时不要配log-impl或者显式配Slf4jImpl。走日志框架管理时只使用logging.level加debug阈值。只有当你明确想通过System.out输出且不关心日志框架整合时才用StdOutImpl。还有一种更隐蔽的情况项目里同时引用了mybatis-plus-boot-starter和mybatis-plus-extension等不同模块各自版本不一致导致配置项的兼容性出现偏差log-impl不生效。这种问题很难从表面看出来排查时最好先统一MyBatis-Plus相关依赖的版本。4.2 多数据源下的配置边界如果你的项目配置了多个数据源比如主库master和从库slave并且两个数据源都使用MyBatis-Plus配置方式会有微妙差别。用logging.level方式时只需要保证两个数据源对应的Mapper接口包路径都在级别配置里logging: level: com.example.demo.master.mapper: debug com.example.demo.slave.mapper: debug用log-impl方式时mybatis-plus.configuration.log-impl是全局生效的没办法单独给某个数据源指定不同的打印方式。如果你两个数据源的日志需求不同比如一个要StdOut、一个要Slf4j就需要分别构建SqlSessionFactory时显式设置Configuration的LogImpl这属于Spring配置高级用法有需要的可以去查SqlSessionFactoryBean的自定义配置这里不展开。值得一提的是如果你只是希望“某个包下打印SQL、另一个包不打印”那用logging.level更容易做到——到mapper包级别设置debug全局保持info。这个粒度比log-impl灵活得多。4.3 线上误开SQL日志的代价我在一个数据敏感的业务模块里处理过一个线上问题运维为了排查一个慢查询直接在生产环境上把logging.level调成了debug跑了一个小时。事后发现日志库多了几个GB的SQL日志而且里面包含了带参数的实际值——个人信息、未加密的状态字段、业务序号全在里面。更麻烦的是这些日志在日志系统里默认保留了30天等于把数据又复制了一份放在日志存储里安全风险远大于排查收益。所以关于生产环境我的建议非常明确不要持续开启SQL日志。如果非开不可用Actuator临时开开完马上关。优先用P6Spy的executionThreshold只记录慢SQL不是每条都打。敏感字段多的表SQL日志里直接会出现明文数据评估风险后再决定开多久。有不少团队会在配置文件里用profile隔离开发环境开启SQL日志测试环境按需开启生产环境完全关闭。这个做法我认可但更稳妥的是再加一层动态开关——需要排查时随时打开排查完立即关闭不用发布新包。5. 把SQL日志收进独立文件并做到随时开关5.1 借助Spring Boot Actuator在线调整日志级别Spring Boot Actuator提供了一个/actuator/loggers端点可以在不重启应用的情况下调整任意logger的日志级别。第一步先暴露端点management: endpoints: web: exposure: include: loggers然后通过接口查询当前mapper包的日志级别curl http://localhost:8080/actuator/loggers/com.example.demo.mapper再通过POST请求临时打开debugcurl -X POST http://localhost:8080/actuator/loggers/com.example.demo.mapper \ -H Content-Type: application/json \ -d {configuredLevel:DEBUG}排查完成后把级别改回INFO即可curl -X POST http://localhost:8080/actuator/loggers/com.example.demo.mapper \ -H Content-Type: application/json \ -d {configuredLevel:INFO}这套组合拳配合logging.level方式使用效果最好。因为SQL日志走的是Slf4j Logback体系Actuator调整的就是这个体系里的logger级别所以开和关都很干净不会留下System.out刷屏的后续影响。也是这一点让我在生产环境需要看SQL时几乎只用这种方式。5.2 使用P6Spy只记录慢SQL如果你不想看每条SQL只想知道“哪些SQL超过1秒”用P6Spy的executionThreshold是最舒服的。在spy.properties里设置executionThreshold1000超过1秒执行的SQL会被标记其他SQL忽略。这种方式既拿到真实SQL又不会刷屏适合日常性能巡检。另外P6Spy还支持exclude配置把某些无关紧要的SQL排除掉比如查询表结构信息的元数据SQLexcludeselect.*from information_schema.*,SELECT.*FROM information_schema.*注意P6Spy默认也会打印连接管理相关的日志如果觉得太吵可以在spy.properties里调整模块启停。这里不逐一列全只提醒一句P6Spy的配置项不算少别指望加了依赖就有理想输出第一次配置花10分钟看下spy.properties的注释是值得的。5.3 和日志框架联动把慢SQL写入独立文件P6Spy默认通过appendercom.p6spy.engine.logging.appender.StdoutLogger输出到控制台。如果你想让它进入日志文件可以改用Slf4jLoggerappendercom.p6spy.engine.logging.appender.Slf4jLogger这样P6Spy的日志会交给Slf4j配合logback可以单独设置logger名字。P6Spy的Slf4jLogger输出到哪个logger默认logger名是p6spy。因此可以在logback里单独给p6spy配置独立appender、独立级别这样生产环境即使开着P6Spy也不会污染主业务日志logger namep6spy levelINFO additivityfalse appender-ref refSQL_FILE/ /logger配合前面的独立SQL文件appender慢SQL就能单独沉淀出来后续做分析、做报表都很方便。这套组合我在一个每日百万级SQL的业务系统上试用过开启后对整体性能的影响不明显但排查问题的效率提升非常明显——以前遇到一个慢SQL要在日志文件里翻半天现在直接查看慢SQL文件按耗时排序一眼锁定。不过还是那句话生产环境需要评估安全风险尤其是数据敏感性高的业务SQL日志本身也是一种数据泄露渠道能少开就少开。写在最后配置简单但机制得吃透我在实际排查项目问题时发现MyBatis-Plus打印SQL这个配置本身不难难的是不同项目里的日志体系不一样配置方式组合起来五花八门。有人直接在application.yml里写log-impl: StdOutImpl就跑通了有人配了logging.level却始终没反应还有人把P6Spy装上去之后发现数据源连接不起来——这些都遇到过。我的建议是不管选哪种方案先把MyBatis日志的两级门控机制想明白第一级是SQL日志交给谁第二级是交给之后能不能打出来。把这两步理清了哪怕换一个日志框架、换一个项目结构也能瞬间反应过来该改哪里而不是一个个配置挨着试。最后分享一个我自己常用的组合本地开发用logging.level加上 Lombok 的Slf4j体系开启时写log-impl: Slf4jImpl配合IDE的Console过滤清爽不刷屏生产环境排查用Actuator端点临时开、临时关慢SQL巡检用P6Spy的executionThreshold全部进独立文件。这套组合我用了很久稳定、干净、可控希望对你有参考价值。