一招搞定! 自定义MyBatis拦截器,SQL日志存储成本直降30%

背景

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

yaml 复制代码
mybatis-plus:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

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

打印出来的 SQL 内容如下。

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, [email protected], TRUE, FALSE, zh-cn, null, null, null, system, 2025-02-10 22:31:03.818, system, null
<==  Total: 1

然而,默认的日志输出格式存在以下不足:

  1. 缺少日志时间,无法快速定位 SQL 执行时间。
  2. SQL 语句可读性差,复杂的 SQL 语句难以阅读。
  3. 日志存储成本高:SQL 模板占用较多字符,增加了日志存储成本。

目标

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

实现

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

java 复制代码
@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();

		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;
	}
}

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

java 复制代码
@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);
	}
}

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

java 复制代码
@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);
	}
}

当项目配置了属性 mybatis.plugin.sql-log.enabled=true 时,SQL 拦截将生效,打印的内容如下。

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_modified_date FROM demo_user WHERE id=1 (10 ms)

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

产出

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

本文涉及的代码完全开源,感兴趣的伙伴可以查阅 eden-mybatis-spring-boot-starter

相关推荐
小斌的Debug日记4 分钟前
SpringBoot和微服务学习记录Day3
spring boot·学习·微服务
嘵奇5 小时前
基于Spring Boot实现文件秒传的完整方案
java·spring boot·后端
caihuayuan55 小时前
JavaScript数据结构与算法实战: 探秘Leetcode经典题目
java·大数据·spring boot·后端·课程设计
LUCIAZZZ6 小时前
KRaft面试思路引导
java·spring boot·算法·面试·kafka·操作系统·raft
码起来呗6 小时前
基于Spring Boot+微信小程序的智慧农蔬微团购平台-项目分享
spring boot·后端·微信小程序
CelestialLuminary366 小时前
Spring Boot的request输入流读取一次即关闭问题解决方案
spring boot
云只上6 小时前
PDF转excel+json ,vue3+SpringBoot在线演示+附带源码
前端·javascript·spring boot·后端·pdf·json·excel
工业互联网专业8 小时前
基于springboot+vue的校园二手物品交易平台
java·vue.js·spring boot·毕业设计·源码·课程设计·校园二手物品交易平台
常年游走在bug的边缘8 小时前
基于spring boot 集成 deepseek 流式输出 的vue3使用指南
java·spring boot·后端·ai
李菠菜9 小时前
SpringBoot+Shiro同服务器多项目Cookie冲突解决方案
spring boot·后端·shiro