找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3249

积分

0

好友

420

主题
发表于 5 小时前 | 查看: 5| 回复: 0

背景

在使用MyBatis Plus时,我们通常通过配置文件设置 log-impl 属性来指定日志实现,从而在控制台打印执行的SQL语句。

mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

logging:
  level:
    org.ylzl.eden.demo.mapper: DEBUG

配置后,打印出来的SQL内容格式如下:

==>  Preparing: SELECT id,login,email,activated,locked,lang_key,activation_key,reset_key,reset_date,created_by,created_date,last_modified_by,last_modified_date FROM demo_user WHERE id=?
==> Parameters: 1(Long)
<==  Columns: ID, LOGIN, EMAIL, ACTIVATED, LOCKED, LANG_KEY, ACTIVATION_KEY, RESET_KEY, RESET_DATE, CREATED_BY, CREATED_DATE, LAST_MODIFIED_BY, LAST_MODIFIED_DATE
<==  Row: 1, admin, 1813986321@qq.com, TRUE, FALSE, zh-cn, null, null, null, system, 2025-02-10 22:31:03.818, system, null
<==  Total: 1

然而,这种默认的日志输出格式在实际生产环境中存在几个明显的不足:

  • 缺少执行时间戳:无法快速定位SQL的具体执行时间,不利于问题排查与性能分析。
  • 可读性较差:SQL语句与参数分离,对于复杂的多条件查询,需要手动拼接才能理解完整的语义,阅读成本高。
  • 存储成本高昂:日志中包含了大量固定的模板字符(如 ==> Preparing:==> Parameters:),这些重复内容显著增加了日志文件的体积,从而推高了存储成本。

目标

为了解决上述问题,我们的目标是绕过MyBatis默认的日志机制,通过自定义 MyBatis拦截器 来直接捕获并格式化SQL信息,输出更简洁、信息更完整的日志。

实现

1. 自定义MyBatis拦截器

首先,我们需要创建一个自定义拦截器,实现 org.apache.ibatis.plugin.Interceptor 接口。这个拦截器的核心作用是拦截Executorqueryupdate方法,在执行前后记录时间,并获取最终要执行的SQL语句。

@Intercepts({
        @Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(method = "query", type = Executor.class, args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
        @Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
})
public class MybatisSqlLogInterceptor implements Interceptor {

    private static final Logger log = LoggerFactory.getLogger("MybatisSqlLog");

    private Duration slownessThreshold = Duration.ofMillis(1000);

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
        String mapperId = mappedStatement.getId();

        // 获取可执行的完整SQL
        String originalSql = MybatisUtils.getSql(mappedStatement, invocation);

        long start = SystemClock.now();
        Object result = invocation.proceed();
        long duration = SystemClock.now() - start;

        // 当 SQL 执行超过我们设置的阈值,转为 WARN 级别
        if (Duration.ofMillis(duration).compareTo(slownessThreshold) < 0) {
            log.info("{} execute sql: {} ({} ms)", mapperId, originalSql, duration);
        } else {
            log.warn("{} execute sql took more than {} ms: {} ({} ms)", mapperId, slownessThreshold.toMillis(), originalSql, duration);
        }
        return result;
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof Executor) {
            return Plugin.wrap(target, this);
        }
        return target;
    }

    // 设置慢 SQL 阈值,单位为秒
    public void setSlownessThreshold(Duration slownessThreshold) {
        this.slownessThreshold = slownessThreshold;
    }
}

2. SQL解析工具类

上面的拦截器依赖一个工具类 MybatisUtils 来解析 MappedStatement 和参数,将带有占位符?的SQL模板还原为参数已填充的、可直接执行的完整SQL字符串。这是实现后端架构中日志优化的关键一步。

@UtilityClass
public class MybatisUtils {

    private static final Pattern PARAMETER_PATTERN = Pattern.compile("\\?");

    public String getSql(MappedStatement mappedStatement, Invocation invocation) {
        Object parameter = null;
        if (invocation.getArgs().length > 1) {
            parameter = invocation.getArgs()[1];
        }
        BoundSql boundSql = mappedStatement.getBoundSql(parameter);
        Configuration configuration = mappedStatement.getConfiguration();
        return resolveSql(configuration, boundSql);
    }

    private static String resolveSql(Configuration configuration, BoundSql boundSql) {
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
        if (!parameterMappings.isEmpty() && parameterObject != null) {
            TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
            if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(resolveParameterValue(parameterObject)));

            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                Matcher matcher = PARAMETER_PATTERN.matcher(sql);
                StringBuffer sqlBuffer = new StringBuffer();
                for (ParameterMapping parameterMapping : parameterMappings) {
                    String propertyName = parameterMapping.getProperty();
                    Object obj = null;
                    if (metaObject.hasGetter(propertyName)) {
                        obj = metaObject.getValue(propertyName);
                    } else if (boundSql.hasAdditionalParameter(propertyName)) {
                        obj = boundSql.getAdditionalParameter(propertyName);
                    }
                    if (matcher.find()) {
                        matcher.appendReplacement(sqlBuffer, Matcher.quoteReplacement(resolveParameterValue(obj)));
                    }
                }
                matcher.appendTail(sqlBuffer);
                sql = sqlBuffer.toString();
            }
        }
        return sql;
    }

    private static String resolveParameterValue(Object obj) {
        if (obj instanceof CharSequence) {
            return "'" + obj + "'";
        }
        if (obj instanceof Date) {
            DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);
            return "'" + formatter.format(obj) + "'";
        }
        return obj == null ? "" : String.valueOf(obj);
    }
}

3. Spring Boot自动配置

为了使拦截器便于管理和配置,我们将其包装为一个Spring Boot Starter的自动配置类,通过配置文件属性来控制其启用和慢SQL阈值。

@AutoConfigureAfter(DataSourceAutoConfiguration.class)
@ConditionalOnBean(SqlSessionFactory.class)
@ConditionalOnProperty(name = "mybatis.plugin.sql-log.enabled")
@EnableConfigurationProperties({MybatisPluginProperties.class})
@RequiredArgsConstructor
@Slf4j
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@Configuration(proxyBeanMethods = false)
public class MybatisPluginAutoConfiguration {

    private final MybatisPluginProperties mybatisPluginProperties;

    @Bean
    public MybatisSqlLogInterceptor mybatisSqlLogInterceptor() {
        MybatisSqlLogInterceptor interceptor = new MybatisSqlLogInterceptor();
        interceptor.setSlownessThreshold(mybatisPluginProperties.getSqlLog().getSlownessThreshold());
        return interceptor;
    }
}

对应的配置属性类:

@Data
@ConfigurationProperties(prefix = "mybatis.plugin")
public class MybatisPluginProperties {

    private final SqlLog sqlLog = new SqlLog();

    @Data
    public static class SqlLog {

        private boolean enabled = true;

        private Duration slownessThreshold = Duration.ofMillis(1000);
    }
}

效果

当在项目的 application.yml 中配置 mybatis.plugin.sql-log.enabled=true 后,SQL拦截器生效,打印的日志格式将变为:

2024-02-10 23:03:01.845 INFO  [dev] [XNIO-1 task-1] org.ylzl.eden.demo.infrastructure.user.database.UserMapper.selectById execute sql: SELECT id,login,email,activated,locked,lang_key,activation_key,reset_key,reset_date,created_by,created_date,last_modified_by,last_modifi    ed_date FROM demo_user WHERE id=1 (10 ms)

这种新的日志格式完全符合生产环境的要求:

  1. 包含完整时间戳:便于日志检索与关联分析。
  2. 输出可执行SQL:参数已直接填充到语句中,一目了然,极大提升了可读性和调试效率。
  3. 记录执行耗时:直观反映SQL性能,并可通过阈值配置自动标记慢查询。

产出

团队在引入这个自定义的SQL日志组件后,在排查线上数据库相关问题时,效率得到了显著提升。更直观的日志格式让问题定位更加清晰。

更重要的是,由于消除了大量冗余的日志模板字符,日志文件的体积平均缩减了约 30%,直接降低了日志存储系统的成本压力。

本文涉及的完整实现代码已开源,感兴趣的朋友可以前往以下仓库查阅和参考:
https://github.com/shiyindaxiaojie/eden-architect/tree/main/eden-components/eden-spring-boot-starters/eden-mybatis-spring-boot-starter

如果你在微服务架构或Java性能优化方面有更多实践经验或想法,欢迎到云栈社区与广大开发者一起交流探讨。

网络表情包:惊讶的卡通人脸




上一篇:开源流媒体聚合工具 Stremio 实战:10.4K Star 项目搭建个人影视中心
下一篇:OpenClaw 技能盘点:10 个核心插件让你的 AI 助手变身高效率数字员工
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 08:47 , Processed in 0.418222 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表