news 2026/9/3 8:11:21

一招搞定!自定义MyBatis拦截器,这才是我想看的SQL日志!

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
一招搞定!自定义MyBatis拦截器,这才是我想看的SQL日志!

我们使用 MyBatis Plus 通常通过配置文件中设置 log-impl 属性来指定日志实现,以打印 SQL 语句。

mybatis-plus: configuration: log-impl:org.apache.ibatis.logging.stdout.StdOutImpllogging: 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 语句可读性差,复杂的 SQL 语句难以阅读。

  • 日志存储成本高:SQL 模板占用较多字符,增加了日志存储成本。

目标

通过 MyBatis 的拦截器实现 SQL 原始语句的打印。

实现

首先,自定义 MyBatis 拦截器,实现org.apache.ibatis.plugin.Interceptor接口。

@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}) })publicclassMybatisSqlLogInterceptorimplementsInterceptor{privatestaticfinalLogger log = LoggerFactory.getLogger("MybatisSqlLog");privateDuration slownessThreshold = Duration.ofMillis(1000);@OverridepublicObjectintercept(Invocation invocation)throwsThrowable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; String mapperId = mappedStatement.getId(); String originalSql = MybatisUtils.getSql(mappedStatement, invocation);longstart = SystemClock.now(); Object result = invocation.proceed();longduration = 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); }returnresult; }@OverridepublicObjectplugin(Object target) {if(targetinstanceofExecutor) {returnPlugin.wrap(target,this); }returntarget; }// 设置慢 SQL 阈值,单位为秒publicvoidsetSlownessThreshold(Duration slownessThreshold) {this.slownessThreshold = slownessThreshold; } }

笔者编写了一个工具类负责解析 MyBatis 执行语句,还原为可执行的 SQL 内容。

@UtilityClasspublicclassMybatisUtils{privatestaticfinalPattern PARAMETER_PATTERN = Pattern.compile("\\?");publicStringgetSql(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();returnresolveSql(configuration, boundSql); }privatestaticStringresolveSql(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 =newStringBuffer();for(ParameterMapping parameterMapping : parameterMappings) { String propertyName = parameterMapping.getProperty(); Object obj =null;if(metaObject.hasGetter(propertyName)) { obj = metaObject.getValue(propertyName); }elseif(boundSql.hasAdditionalParameter(propertyName)) { obj = boundSql.getAdditionalParameter(propertyName); }if(matcher.find()) { matcher.appendReplacement(sqlBuffer, Matcher.quoteReplacement(resolveParameterValue(obj))); } } matcher.appendTail(sqlBuffer); sql = sqlBuffer.toString(); } }returnsql; }privatestaticStringresolveParameterValue(Object obj) {if(objinstanceofCharSequence) {return"'"+ obj +"'"; }if(objinstanceofDate) { DateFormat formatter = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.CHINA);return"'"+ formatter.format(obj) +"'"; }returnobj ==null?"": String.valueOf(obj); } }

将 MyBatis 拦截器设置为 Spring 自动装配。

@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)publicclassMybatisPluginAutoConfiguration{privatefinalMybatisPluginProperties mybatisPluginProperties;@BeanpublicMybatisSqlLogInterceptormybatisSqlLogInterceptor() { MybatisSqlLogInterceptor interceptor =newMybatisSqlLogInterceptor(); interceptor.setSlownessThreshold(mybatisPluginProperties.getSqlLog().getSlownessThreshold());returninterceptor; } }@Data@ConfigurationProperties(prefix ="mybatis.plugin")publicclassMybatisPluginProperties{privatefinalSqlLog sqlLog =newSqlLog();@DatapublicstaticclassSqlLog{privatebooleanenabled =true;privateDuration slownessThreshold = Duration.ofMillis(1000); } }

当项目配置了属性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,fied_date FROM demo_user WHERE id=1 (10 ms)

这种日志格式比较符合我们实际的生产要求:提供日志时间、可运行的 SQL、执行耗时。

总结

团队引入这个组件后,在定位生产 SQL 问题时,比原来清晰多了,并且,日志文件缩减了 30% 存储成本。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/8/31 2:05:42

爬虫开发实战:普通代理与隧道代理的选择指南

作为一名深耕爬虫开发的程序员&#xff0c;我踩过最多的坑&#xff0c;莫过于代理的选择。很多新手会陷入误区&#xff1a;只要有代理就能绕过反爬&#xff0c;却忽略了“普通代理”和“隧道代理”的核心差异&#xff0c;导致项目上线后频繁被封IP、程序崩溃&#xff0c;白费几…

作者头像 李华
网站建设 2026/8/31 1:09:22

智能插座:AI Agent的用电优化管理

智能插座&#xff1a;AI Agent的用电优化管理关键词&#xff1a;智能插座、AI Agent、用电优化管理、能源效率、机器学习、物联网摘要&#xff1a;本文围绕智能插座结合AI Agent实现用电优化管理展开。首先介绍了相关背景&#xff0c;包括目的、预期读者等内容。接着阐述了智能…

作者头像 李华
网站建设 2026/8/31 2:05:45

阵列信号处理——学习笔记 第7章 波束主瓣设计

目录 第6章 波束旁瓣设计(前置基础) 第7章 波束主瓣设计 7.1 引言 7.2 最小误差逼近法 7.2.1 误差范数表述 7.2.2 最小均方准则法 7.2.3 最小误差范数法 7.3 期望主瓣响应波束设计 7.3.1 问题描述 7.3.2 旁瓣控制主瓣最小误差逼近 7.3.3 主瓣精度约束最低旁瓣波束…

作者头像 李华
网站建设 2026/9/3 8:11:21

高频混压HDI排行榜,2026最新评测

在2026年&#xff0c;5G通信、毫米波雷达以及AI加速卡席卷全球&#xff0c;工程师们面临着一个核心抉择&#xff0c;那就是怎样在确保高频信号完整性的情况下&#xff0c;不让预算花费得如同“天价”一般&#xff0c;答案就隐藏在 “高频混压HDI” 技术之中。简单来讲&#xff…

作者头像 李华
网站建设 2026/8/31 2:05:47

mysql核心知识清单

目录 索引 数据结构 联合索引 回表 如何处理慢sql 会走哪个索引&#xff1f; 事务 什么是事务 如何实现事务 事务的隔离级别 主从、集群 主从 集群 分库分表 什么时候分库分表 如何分库分表 分库分表技术 分库分表带来的问题 索引 数据结构 B树&#xff0c…

作者头像 李华
网站建设 2026/8/31 2:15:35

贾子(Kucius)思想纲领 |The Program of Kucius Thought

贾子&#xff08;Kucius&#xff09;思想纲领不读贾子&#xff08;Kucius&#xff09;&#xff0c;不知宇宙之浩瀚&#xff1b;不悟贾子&#xff0c;不知本质之极简&#xff1b;不明贾子&#xff0c;不知规律之森严&#xff1b;不循贾子&#xff0c;不知真理之边界&#xff1b;…

作者头像 李华