SpringBoot3+MyBatisPlus日志集成:SQL输出与故障排查详解
1. 为什么要把日志功能单独拿出来做咱们这个系列走到第二篇项目的底座已经搭起来了SpringBoot3 能启动MyBatisPlus 连上了数据库基础 CRUD 也能跑了。这时候如果不加日志开发时靠 debug 断点还能勉强撑过去等到功能一多、接口一复杂、或者联调环境开始跑数据你会发现自己跟个瞎子一样——接口报错了不知道哪一步出的问题SQL 执行了不知道传了什么参数分页查出来数据不对也不知道是 SQL 的问题还是插件的问题。所以这一篇说的“加入日志功能”并不是往代码里多打几行 System.out 就完了而是要把两层日志完整接进来。第一层是应用运行日志也就是 SpringBoot 启动过程、请求处理、异常堆栈这些输出。第二层是 MyBatisPlus 的 SQL 执行日志也就是每次访问数据库到底执行了哪些 SQL绑定了哪些参数返回了多少行。这两层一个管宏观一个管微观加起来才能组成排查问题的完整现场。如果你正处于这两个阶段之一刚搭完一个 SpringBoot3 MyBatisPlus 项目想在正式开发前把日志体系补齐或者项目已经能跑起来但控制台输出要么乱成一团、要么一条 SQL 都看不到那这篇内容就是给你准备的。文章会从日志框架选型、依赖配置、logback-spring.xml 编写到 MyBatisPlus 的 log-impl 配置再到分页失效和单页 500 条限制这些连带问题一次性讲清楚。再说一下本文的前置环境JDK 17 及以上SpringBoot 3.xMyBatisPlus 使用 3.5.x 版本数据库用 MySQL项目里有任意一个能正常调用的 Mapper 接口。版本不一样的话依赖坐标和个别配置项的写法会有出入但整体思路是一样的。2. 先理清日志链路门面、实现、还有 MyBatis 的 log-impl很多人一上来就在 application.yml 里配log-impl结果要么不生效要么换了 log4j2 之后启动直接报 SLF4J 绑定冲突要么 SQL 日志在本地能看到、部署到服务器上却没了。这些问题归根结底是没有搞明白日志链路里三层东西的关系。2.1 SLF4J、Logback 与 Log4j2 的关系SLF4J 只是一个日志门面它本身不负责输出日志只定义了一套统一的日志接口。真正的输出工作要交给具体的日志实现SpringBoot3 默认用的是 Logback。自动引入的spring-boot-starter-logging会把 logback-classic 和 SLF4J API 一起带进来。所以你在代码里写private static final Logger log LoggerFactory.getLogger(Xxx.class)实际上调用的是 SLF4J 门面的 API再由它转发给 Logback 去写文件、写控制台。这套机制的好处是代码层面不绑定具体实现以后想从 Logback 换成 Log4j2理论上不用改任何业务代码只改依赖和配置就行。但注意有个坑SLF4J 只是一个门面classpath 里如果同时存在多个日志实现或者存在多份 SLF4J 绑定就会有冲突。启动时看到SLF4J: Class path contains multiple SLF4J bindings这种告警十有八九是依赖没管住。这也是为什么换 Log4j2 不能简单“加一个依赖”就完事必须把 SpringBoot 自动引入的 Logback 先排除掉。2.2 MyBatis 的日志适配器机制MyBatis 自己并不直接依赖某个具体日志框架它内部有一套 LogFactory 机制按优先级去找可用的日志实现同时允许通过配置强制指定。MyBatisPlus 作为 MyBatis 的增强框架把这个配置项暴露成了mybatis-plus.configuration.log-impl。常用的内置实现有三个配置值输出方式适用场景org.apache.ibatis.logging.stdout.StdOutImpl直接 System.out 打印绕过日志框架不分级别本地开发快速看 SQLorg.apache.ibatis.logging.slf4j.Slf4jImpl委托给 SLF4J以 debug 级别输出日志名是 Mapper 接口的全限定名统一纳入日志框架可写文件、可分级org.apache.ibatis.logging.nologging.NoLoggingImpl不输出彻底关闭 SQL 日志这里要特别理解一点StdOutImpl不走 SLF4J所以它不受 logback 配置控制无论日志级别配成 INFO 还是 ERROR该打印还是打印而且也不会进日志文件。生产环境如果用这个SQL 只会输出到控制台经过容器管理后可能直接丢失很难追溯。所以正经项目我更推荐Slf4jImpl让 MyBatis 的 SQL 日志也汇入统一的日志链路交给 logback 管理。还有一个关键细节当使用Slf4jImpl时MyBatis 输出的日志记录器名称是 Mapper 接口的完全限定名。比如你的接口是com.example.demo.mapper.SysUserMapper那 SQL 日志就打在名为com.example.demo.mapper.SysUserMapper的 logger 上。所以要在 logback 里把com.example.demo.mapper这个包级别调到 debug否则哪怕log-impl配了 Slf4jImpl也会因为日志级别是 info 而被过滤掉。这是新手最容易忽略的一步。3. MyBatisPlus 日志功能集成实操从依赖到 application.yml理论清楚了下面开始动手。先说依赖再说配置最后验证输出。3.1 依赖版本与 Maven 配置先确认项目用的是 SpringBoot3。当前 MyBatisPlus 官方把启动器按 Boot 版本拆分了SpringBoot 2.x 用mybatis-plus-boot-starterSpringBoot 3.x 用mybatis-plus-spring-boot3-starter。我这边项目用的版本是 3.5.7依赖这样写dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.7/version /dependency这个坐标必须跟 SpringBoot3 对齐。如果项目里混用了 SpringBoot2 时代的 starter最常见的现象就是 Mapper 接口扫描正常、CRUD 也能跑但分页插件不生效SQL 日志也时有时无。原因在于 MyBatis 和 SpringBoot3 的兼容层版本不对很多时候还牵扯到jakarta.*命名空间的问题。至于日志依赖SpringBoot3 项目里只要引入了 web 或其他基础 starter就已经自带spring-boot-starter-logging也就是 SLF4J Logback 这套组合。除非你想换 Log4j2否则不需要额外加日志依赖。3.2 application.yml 关键配置项目的基础 MyBatisPlus 配置一般长这样spring: application: name: demo-project mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml type-aliases-package: com.example.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.slf4j.Slf4jImpl logging: level: com.example.demo.mapper: debug重点看两个地方。第一mybatis-plus.configuration.log-impl指定为 Slf4jImpl这样 SQL 日志会交给 SLF4J 统一输出。第二logging.level.com.example.demo.mapper要配成 debug。注意这里的包名是实际项目里的 Mapper 接口所在包不是随便抄的。如果只是想在本地控制台快速看 SQL不想折腾日志文件也可以临时把log-impl换成org.apache.ibatis.logging.stdout.StdOutImpl。这种情况下不需要配logging.level.com.example.demo.mapper因为 StdOutImpl 不当日志级别存在直接打印。3.3 首次运行与输出验证配置改完后重启项目随便调用一个 Mapper 方法。假如查询SysUserMapper.selectById(1L)控制台或日志文件里预期会出现类似下面的输出 Preparing: SELECT id,user_name,age FROM sys_user WHERE id? Parameters: 1(Long) Columns: id, user_name, age Row: 1, zhangsan, 25 Total: 1Preparing后面是要执行的 SQL 语句占位符仍然是?Parameters里是绑定到占位符上的实际参数值Columns是返回结果的列名Row是每一行数据Total是本次查询返回的总行数。如果Preparing和Parameters都看到了说明 MyBatisPlus 的日志链路已经打通。如果一条 SQL 日志都没看到按这个顺序排查先确认真的调用了 Mapper 方法再确认log-impl配置没写错类名最后确认 mapper 包的 logging level 是 debug。这三个环节任何一个断了日志都不会出来。4. logback-spring.xml滚动日志与多环境配置详解日志链路打通之后下一步就是决定日志往哪里写、怎么写。开发环境往控制台打没问题但生产环境需要把日志落盘还要按天或按大小滚动防止单个文件无限增长撑爆磁盘。这块就要用到 logback 的配置文件。4.1 为什么推荐 logback-spring.xml 而不是 logback.xmlSpringBoot 项目里日志配置文件的命名是有讲究的。如果类路径下有logback.xmlLogback 会直接加载它但它不认识 SpringBoot 的扩展标签比如springProfile、springProperty想做多环境差异化配置就很别扭。而且 SpringBoot 有自己的日志初始化流程logback.xml这种纯 Logback 配置文件有时候会被 SpringBoot 的配置覆盖或部分忽略就容易出现“改了配置文件但没生效”的灵异事件。更推荐的做法是用logback-spring.xml。这个文件名带-spring后缀SpringBoot 会完整接管它的初始化过程支持springProfile标签可以针对不同环境加载不同配置。配置文件放在src/main/resources目录下启动时自动识别不需要额外指定。4.2 一份可直接落地的配置样例下面这份配置我用了很多次适合大多数 SpringBoot3 MyBatisPlus 项目?xml version1.0 encodingUTF-8? configuration !-- 日志根目录和输出格式 -- property nameLOG_HOME value./logs/ property namePATTERN value%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/ !-- 控制台输出 -- appender nameCONSOLE classch.qos.logback.core.ConsoleAppender encoder pattern${PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- 滚动文件输出 -- appender nameFILE classch.qos.logback.core.rolling.RollingFileAppender file${LOG_HOME}/app.log/file rollingPolicy classch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy fileNamePattern${LOG_HOME}/app-%d{yyyy-MM-dd}.%i.log/fileNamePattern maxFileSize10MB/maxFileSize maxHistory30/maxHistory totalSizeCap1GB/totalSizeCap /rollingPolicy encoder pattern${PATTERN}/pattern charsetUTF-8/charset /encoder /appender !-- Mapper 包单独开 debug方便看 SQL -- logger namecom.example.demo.mapper leveldebug additivityfalse appender-ref refCONSOLE/ appender-ref refFILE/ /logger !-- Spring 框架日志降噪 -- logger nameorg.springframework levelINFO/ !-- 根日志级别 -- root levelINFO appender-ref refCONSOLE/ appender-ref refFILE/ /root /configuration这份配置里最核心的是滚动策略。maxFileSize表示单个日志文件达到 10MB 就切分文件名里的%i用来区分同一个日期下切出来的多个文件maxHistory表示最多保留 30 天totalSizeCap表示所有日志文件总大小不能超过 1GB超过后删除最旧的。这三个参数组合使用既能保证日志够查又不会让服务器磁盘被日志吃光。additivityfalse的意思是这个 logger 输出时不再向父 logger 重复传递避免 Mapper 包的 debug 日志被重复打印两遍。如果你发现 SQL 日志在控制台刷了两遍多半就是 additivity 没设对。4.3 切换 Log4j2 的正确姿势有的团队以前用 Log4j2 用惯了想在新项目里继续用。这个完全可以但必须按下面的方式切换。首先排除掉 SpringBoot 默认的 Logback 依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId exclusions exclusion groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-logging/artifactId /exclusion /exclusions /dependency再加入 Log4j2 的 starterdependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-log4j2/artifactId /dependency然后放一个log4j2.xml在类路径下内容示例?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console RollingFile nameRollingFile fileName./logs/app.log filePattern./logs/app-%d{yyyy-MM-dd}.%i.log PatternLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n/ Policies SizeBasedTriggeringPolicy size10MB/ /Policies DefaultRolloverStrategy max30/ /RollingFile /Appenders Loggers Logger namecom.example.demo.mapper leveldebug additivityfalse AppenderRef refConsole/ AppenderRef refRollingFile/ /Logger Root levelinfo AppenderRef refConsole/ AppenderRef refRollingFile/ /Root /Loggers /Configuration换成 Log4j2 后MyBatisPlus 的log-impl仍然可以配Slf4jImpl因为 SLF4J 门面会找到 Log4j2 的实现来输出。但务必检查依赖树确保spring-boot-starter-logging已经被排干净。我见过有人在排除之后又通过别的模块间接引入了 Logback结果启动时 SLF4J 绑定混乱SQL 日志一会儿正常一会儿消失非常难查。5. SQL 日志进阶查看参数、执行时长与慢 SQL大多数人接入日志功能目的不只是“能看到 SQL”而是能拿日志去排查问题。这一节把 SQL 日志的深入用法讲一下。5.1 标准 SQL 日志到底该怎么看前面提到过MyBatis 的 SQL 日志格式是Preparing、Parameters、Columns、Row、Total。这套格式其实非常经典理解透了能解决很多问题。Preparing打印的是 JDBC 预处理语句是带?占位符的 SQL。真正执行的时候Parameters就是绑定到?上的值。比如Parameters: 1(Long)说明第一个占位符绑定了 Long 类型的 1。注意看类型如果类型转换不对例如 Long 被当成 String 传进去数据库索引就可能失效查询变慢。Total: 1是执行后影响或返回的行数。这个值对 UPDATE、DELETE 操作尤其重要。明明删了一条数据如果Total是 0说明条件没匹配到如果Total是 100说明影响范围超出预期得立刻检查 SQL 条件。还有个常见问题Preparing能看到Parameters有时候看不到尤其是使用自定义 TypeHandler 或者自己包装了参数对象的时候。这种情况建议加一个拦截器去统一打印完整参数后面会讲。5.2 自定义拦截器打印完整 SQL 与执行耗时标准日志已经很好用了但它不打印执行耗时也不会自动输出一条“可以直接复制到数据库客户端执行”的完整 SQL。这里可以用 MyBatis 的拦截器扩展。下面是一个简单的 SQL 耗时日志拦截器import lombok.extern.slf4j.Slf4j; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.BoundSql; import org.apache.ibatis.plugin.Interceptor; import org.apache.ibatis.plugin.Intercepts; import org.apache.ibatis.plugin.Invocation; import org.apache.ibatis.plugin.Signature; import java.sql.Statement; Slf4j Intercepts({ Signature(type StatementHandler.class, method query, args {Statement.class, ResultHandler.class}), Signature(type StatementHandler.class, method update, args {Statement.class}) }) public class SqlCostInterceptor implements Interceptor { Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); try { return invocation.proceed(); } finally { long cost System.currentTimeMillis() - start; StatementHandler handler (StatementHandler) invocation.getTarget(); BoundSql boundSql handler.getBoundSql(); log.info(SQL 执行耗时: {} ms, SQL: {}, cost, boundSql.getSql()); } } }把拦截器注册成 Spring Bean 即可生效Configuration public class MybatisInterceptConfig { Bean public SqlCostInterceptor sqlCostInterceptor() { return new SqlCostInterceptor(); } }这个拦截器虽然简单但抓慢 SQL 非常有效。把耗时超过阈值的方法单独打印出来配合日志链路定位就能逐步找到接口响应慢的根因。至于打印完整 SQL替换掉?和敏感字段脱敏实现要复杂很多因为参数对象可能是一个实体类也可能是一个 Map也可能是单个基本类型需要针对不同场景递归处理。建议在标准日志能满足大部分需求的情况下先不要过度设计等真正遇到问题时再补充。6. 两个热门问题排查现场分页失效与单页 500 条限制接手这个系列项目的时候我在搜索相关 QA 时发现两个高频问题MyBatisPlus 分页失效以及单页查询被限制在 500 条。这两个问题恰好和日志功能关系密切因为它们的排查都离不开 SQL 日志。6.1 MyBatisPlus 分页失效的原因清单先说结论在 MyBatisPlus 3.5.x 中分页必须依靠MybatisPlusInterceptor和PaginationInnerInterceptor来生效。如果你直接在项目里调用selectPage但不注册拦截器SQL 日志里的查询语句不会出现LIMIT关键字MP 会在内存里把所有匹配数据加载出来再切片。正确配置长这样Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); interceptor.addInnerInterceptor(pagination); return interceptor; } }分页失效常见原因我整理过一份清单没有注册MybatisPlusInterceptor或者注册了但没加PaginationInnerInterceptor。还在用旧版的PaginationInterceptor。这个类在 3.5.x 里已经移除了配置类里如果还引用它分页不会生效。多数据源场景下拦截器只注入到了某个SqlSessionFactory另一个数据源的 Mapper 没有拦截器。自定义了SqlSessionFactory或MybatisSqlSessionFactoryBean但没有把这个拦截器加进去。XML 里手写 SQL 时自己加了LIMIT表面上看也分页了但统计总数功能失效。排查方法很简单把日志功能打开调用selectPage看日志里有没有LIMIT。如果查询语句不包含 LIMIT说明分页拦截器没有生效按清单逐项排查。6.2 单页 500 条限制怎么解除另一个高频问题是“查 1000 条数据只返回 500 条”。这个问题和日志的关系更直接因为 SQL 日志会打印出真实的LIMIT值。很多项目在配置分页插件时会顺手加一句pagination.setMaxLimit(500L);这个设置的含义是单页查询最多返回 500 行超过的部分自动截断。如果前端一次要查 1000 条做导出或批量处理就会被悄悄砍成 500 条。你从业务代码上根本看不出问题因为代码写的是 size1000但日志里 SQL 末尾明确带着LIMIT 500这时候立刻就能定位到是分页插件的限制。解除方式有两个要么按业务场景重新评估把maxLimit调大要么不限制改成-1LPaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(-1L);这里有一点要注意很多数据库客户端和管理工具有预览功能默认只读取前 500 条或前 1000 条这是客户端层面的限制跟 MyBatisPlus 没有任何关系。如果 SQL 日志里没有出现LIMIT 500但页面或工具里看起来只显示 500 行先检查是不是客户端预览限制。通过这两个案例你会发现日志功能看着不起眼但遇到分页这类怪问题时SQL 日志几乎是唯一能直接给出答案的手段。没有日志你可能要花几个小时在代码里加打印、反复重启、甚至怀疑数据库数据有问题。7. 实操心得与几个小建议接完这个日志功能后有几个经验值得单独记一下。第一开发环境建议直接用StdOutImpl别在开发阶段就为日志文件折腾。开发时最重要的是快速看到 SQL控制台打印最直接。到了联调或准备上生产再切换成Slf4jImpl。切换只需要改一行配置成本很低。第二生产环境不要长时间把 Mapper 日志级别开在 debug。SQL 日志一旦全量输出高并发请求下日志量会非常惊人磁盘 IO 和存储都会被消耗。我的做法是日常开 info遇到线上问题时临时把某个 Mapper 包调到 debug问题定位后再改回来。配合 SpringBoot 的配置中心这个操作可以做到动态调整不需要重启。第三日志里会包含查询参数如果业务涉及用户手机号、身份证、地址等敏感字段SQL 日志会成为敏感信息泄露的通道。简单粗暴的办法是对相关字段做掩码处理或者把含敏感字段的 Mapper 包日志级别保持为 info。第四日志打印要控制节奏。MyBatisPlus 的 Mapper 日志可以用但频繁调用的热点接口SQL 日志会占日志文件的大量空间。可以结合拦截器只对执行时长超过阈值比如 1 秒的 SQL 做记录这样日志量和可用性之间更容易平衡。第五给日志加上 traceId。目前的配置能满足单机排查但一旦部署多实例一个请求打到不同的机器日志就分散了。比较推荐的做法是引入一个请求拦截器为每次请求生成唯一 traceId放进 MDC然后在PATTERN里输出。这个扩展并不复杂但会让日志检索效率提升一大截。日志功能做到这一步已经不再只是“有日志可看”而是能支撑日常开发、问题定位、性能排查的完整基础能力。后续如果项目继续深入可以在这个基础上加访问日志、动态日志级别、告警联动等但最重要的第一步是把链路打通、把配置文件理清楚。这篇文章里给的配置和排查思路足够你在实际项目里少走不少弯路了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →