MyBatis-Plus SQL日志打印配置与优化指南
发布时间:2026/9/14 8:02:55 作者:尧图编辑部 阅读量:1,286

1. MyBatis-Plus SQL日志打印的必要性在开发过程中SQL语句的可见性对于调试和性能优化至关重要。MyBatis-Plus作为MyBatis的增强工具提供了多种灵活的SQL日志打印配置方式。想象一下这样的场景你在调试一个复杂的多表关联查询时控制台突然抛出一个模糊的错误信息但却看不到实际执行的SQL语句这种盲调状态会让问题排查变得异常困难。SQL日志打印不仅能帮助开发者快速定位问题还能在以下场景中发挥关键作用验证动态SQL是否按预期生成检查查询条件是否正确拼接分析N1查询问题优化慢查询性能审计数据变更操作2. 基础配置方式2.1 全局日志级别配置最简单的配置方式是在application.yml或application.properties中设置全局日志级别# application.yml配置示例 logging: level: com.baomidou.mybatisplus: debug或者使用properties格式# application.properties配置示例 logging.level.com.baomidou.mybatisplusdebug这种配置会将MyBatis-Plus相关类的日志级别统一设置为DEBUG包括但不限于SQL语句参数绑定执行结果事务操作注意生产环境慎用此配置可能产生大量日志影响性能。建议通过条件注解(如Profile)限制只在开发环境启用。2.2 精确控制Mapper接口日志如果只需要特定Mapper接口的SQL日志可以针对具体接口配置Mapper Repository public interface UserMapper extends BaseMapperUser { // 接口方法 } // 在配置类中添加 Configuration public class MyBatisPlusConfig { Bean public Logger userMapperLogger() { return LoggerFactory.getLogger(UserMapper.class); } }然后在日志配置中logging: level: com.example.mapper.UserMapper: debug这种方式的好处是日志输出更有针对性避免无关SQL干扰可以按业务模块区分日志级别3. 高级配置方案3.1 自定义SQL日志格式化MyBatis-Plus默认的SQL日志格式可能不符合团队规范可以通过实现ISqlInjector接口自定义public class CustomSqlInjector extends DefaultSqlInjector { Override public void inspectInject(MapperBuilderAssistant builderAssistant, Class? mapperClass) { // 自定义SQL注入逻辑 } } Configuration public class MyBatisPlusConfig { Bean public MybatisPlusSqlInjector mybatisPlusSqlInjector() { return new CustomSqlInjector(); } }结合日志框架的PatternLayout可以定义如下的日志格式# logback-spring.xml示例 appender nameSTDOUT classch.qos.logback.core.ConsoleAppender encoder pattern%d{yyyy-MM-dd HH:mm:ss} | %-5level | %logger{36} | %msg%n/pattern /encoder /appender3.2 动态日志级别切换对于需要临时开启SQL日志的生产环境问题排查可以结合Spring Boot Actuator实现动态调整RestController RequestMapping(/admin/logging) public class LoggingController { PostMapping(/sql) public String toggleSqlLogging(RequestParam boolean enable) { LoggerContext loggerContext (LoggerContext) LoggerFactory.getILoggerFactory(); Logger logger loggerContext.getLogger(com.baomidou.mybatisplus); logger.setLevel(enable ? Level.DEBUG : Level.INFO); return SQL logging (enable ? enabled : disabled); } }这样就能通过API动态控制SQL日志的开关# 开启SQL日志 curl -X POST http://localhost:8080/admin/logging/sql?enabletrue # 关闭SQL日志 curl -X POST http://localhost:8080/admin/logging/sql?enablefalse4. 性能优化与注意事项4.1 日志输出性能影响虽然SQL日志对开发很有帮助但不恰当的配置可能带来性能问题I/O瓶颈频繁的日志写入可能成为系统瓶颈内存消耗大量SQL参数会占用堆内存网络开销远程日志收集时可能产生额外延迟优化建议使用异步Appender如Logback的AsyncAppender限制单条SQL日志的最大长度生产环境只打印WARN级别以上的日志4.2 敏感数据防护SQL日志可能暴露敏感信息需要特别注意// 自定义参数过滤器 public class SensitiveDataFilter implements ParameterFilter { Override public Object processParameter(Object parameter) { if (parameter instanceof String) { String str (String) parameter; if (str.matches(\\d{16,19})) { // 匹配银行卡号 return ****-****-****- str.substring(str.length()-4); } } return parameter; } } // 配置到MyBatisPlus Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new SensitiveDataFilter()); return interceptor; }4.3 多数据源下的日志配置对于多数据源项目需要为每个数据源单独配置日志Configuration public class DataSourceConfig { Primary Bean public DataSource primaryDataSource() { return DataSourceBuilder.create() .type(HikariDataSource.class) .url(jdbc:mysql://primary-db:3306/db) .build(); } Bean public DataSource secondaryDataSource() { return DataSourceBuilder.create() .type(HikariDataSource.class) .url(jdbc:mysql://secondary-db:3306/db) .build(); } Bean Primary public SqlSessionFactory primarySqlSessionFactory( Qualifier(primaryDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 主数据源使用DEBUG级别 factory.setConfiguration(getConfiguration(LogLevel.DEBUG)); return factory.getObject(); } Bean public SqlSessionFactory secondarySqlSessionFactory( Qualifier(secondaryDataSource) DataSource dataSource) throws Exception { SqlSessionFactoryBean factory new SqlSessionFactoryBean(); factory.setDataSource(dataSource); // 从数据源使用WARN级别 factory.setConfiguration(getConfiguration(LogLevel.WARN)); return factory.getObject(); } private org.apache.ibatis.session.Configuration getConfiguration(LogLevel logLevel) { org.apache.ibatis.session.Configuration config new org.apache.ibatis.session.Configuration(); config.setLogImpl(StdOutImpl.class); // 使用标准输出日志 config.setLogPrefix([MYBATIS] ); // 日志前缀 config.setLogLevel(logLevel); // 日志级别 return config; } }5. 常见问题排查5.1 日志不输出问题排查当配置正确但SQL日志没有输出时可以按照以下步骤排查检查依赖是否完整!-- 必须包含mybatis-plus-boot-starter -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version最新版本/version /dependency !-- 以及日志实现如logback -- dependency groupIdch.qos.logback/groupId artifactIdlogback-classic/artifactId /dependency验证日志框架是否生效SpringBootTest public class LoggingTest { private static final Logger log LoggerFactory.getLogger(LoggingTest.class); Test void testLogOutput() { log.trace(Trace message); log.debug(Debug message); log.info(Info message); log.warn(Warn message); log.error(Error message); } }检查是否有多个日志框架冲突如同时存在logback和log4j2确认没有在代码中动态修改日志级别5.2 日志内容不完整问题如果SQL日志缺少参数或执行结果可能是以下原因MyBatis原生配置覆盖mybatis: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl拦截器影响了日志输出// 检查是否有自定义拦截器修改了SQL Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 确认各InnerInterceptor的实现 return interceptor; }日志框架的过滤器设置!-- 检查logback配置中是否有过滤器 -- filter classch.qos.logback.core.filter.ThresholdFilter levelINFO/level /filter6. 生产环境最佳实践6.1 按需采集SQL日志生产环境推荐使用以下策略通过采样率控制日志量如1%的请求打印完整SQL只记录慢查询执行时间超过阈值使用MDC标记特定会话的日志实现示例public class SamplingSqlLogger implements SqlLogger { private static final Random RANDOM new Random(); private final double samplingRate; public SamplingSqlLogger(double samplingRate) { this.samplingRate samplingRate; } Override public void log(String sql) { if (RANDOM.nextDouble() samplingRate) { LoggerFactory.getLogger(SQL_SAMPLING).info(sql); } } } // 配置使用 Bean public SamplingSqlLogger samplingSqlLogger() { return new SamplingSqlLogger(0.01); // 1%采样率 }6.2 结构化日志输出将SQL日志转换为结构化数据更利于分析{ timestamp: 2023-07-20T14:30:00Z, level: DEBUG, logger: com.baomidou.mybatisplus, thread: http-nio-8080-exec-1, message: Executing SQL, context: { sql: SELECT * FROM user WHERE id ?, params: [123], executionTime: 45, dataSource: primary } }配置方法以Logback为例appender nameJSON classch.qos.logback.core.ConsoleAppender encoder classnet.logstash.logback.encoder.LogstashEncoder/ /appender6.3 与APM工具集成将SQL日志与应用性能监控(APM)系统集成使用SkyWalking自动采集# application.yml spring: application: name: your-service cloud: skywalking: enabled: true agent: service_name: ${spring.application.name} collector: backend_service: skywalking-oap:11800通过Micrometer暴露指标Bean public MeterRegistryCustomizerMeterRegistry metricsCommonTags() { return registry - registry.config().commonTags( application, your-service, region, System.getenv(REGION) ); }在Grafana等监控平台创建SQL性能仪表盘7. 扩展功能开发7.1 SQL日志审计功能实现一个完整的SQL审计功能需要定义审计实体Entity Table(name sql_audit_log) public class SqlAuditLog { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String sql; private String parameters; private long executionTime; private String dataSource; private String username; private String clientIp; private Timestamp createTime; // getters/setters }实现审计拦截器public class AuditInterceptor implements Interceptor { private final AuditLogService auditLogService; Override public Object intercept(Invocation invocation) throws Throwable { long start System.currentTimeMillis(); Object result invocation.proceed(); long end System.currentTimeMillis(); MappedStatement ms (MappedStatement) invocation.getArgs()[0]; Object parameter invocation.getArgs()[1]; SqlAuditLog log new SqlAuditLog(); log.setSql(ms.getBoundSql(parameter).getSql()); log.setParameters(parameter.toString()); log.setExecutionTime(end - start); // 设置其他字段... auditLogService.save(log); return result; } }注册拦截器Bean public AuditInterceptor auditInterceptor(AuditLogService auditLogService) { return new AuditInterceptor(auditLogService); } Bean public MybatisPlusInterceptor mybatisPlusInterceptor(AuditInterceptor auditInterceptor) { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(auditInterceptor); return interceptor; }7.2 基于SQL日志的自动化测试利用SQL日志生成测试用例public class SqlTestGenerator { public void generateTestFromLogs(String logFile) throws IOException { ListString sqls Files.lines(Paths.get(logFile)) .filter(line - line.contains(Preparing:)) .map(line - line.split(Preparing:)[1].trim()) .collect(Collectors.toList()); for (String sql : sqls) { String testName test sql.hashCode(); String testContent generateTestContent(sql); Files.write(Paths.get(src/test/java/ testName .java), testContent.getBytes()); } } private String generateTestContent(String sql) { return public class testName {\n Test\n void verifySql() {\n // 验证SQL: sql \n }\n }; } }7.3 SQL性能分析插件开发一个分析SQL性能的插件public class PerformanceInterceptor implements InnerInterceptor { private final PerformanceAnalyzer analyzer; Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { analyzer.startTimer(ms.getId()); } Override public void afterQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql, ListObject resultList) { analyzer.recordQuery(ms.getId(), boundSql.getSql()); } } public class PerformanceAnalyzer { private final MapString, Long startTimes new ConcurrentHashMap(); private final MapString, ListLong executionTimes new ConcurrentHashMap(); public void startTimer(String statementId) { startTimes.put(statementId, System.currentTimeMillis()); } public void recordQuery(String statementId, String sql) { long duration System.currentTimeMillis() - startTimes.get(statementId); executionTimes.computeIfAbsent(statementId, k - new ArrayList()).add(duration); // 分析慢查询 if (duration 1000) { // 1秒阈值 warnSlowQuery(statementId, sql, duration); } } public MapString, PerformanceStats getStats() { return executionTimes.entrySet().stream() .collect(Collectors.toMap( Map.Entry::getKey, e - new PerformanceStats( e.getValue().stream().mapToLong(l - l).average().orElse(0), e.getValue().size() ) )); } }