通过实现MyBatis的Interceptor接口在SQL头部增加统一注释

背景

从事运维或DBA工作的童鞋会非常熟悉在SQL前部增加注释的操作。类似如下的SQL语句:

/* appUk:[testapp];host ip:[192.168.1.111];traceId:[dcb7f7a0cbe72817];spanId[dcb7f7a0cbe72817] */
INSERT INTO test_table ( project_id, tenant, c_project_id, g_ra_type, g_ra_version, g_ra_config, create_by, update_by ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ? )

这种注释虽然不会影响SQL执行,但是会为运维和DBA工作带来极大的便利如:

  • 对慢查询进行优化时,可以通过注释信息快速找到研发团队及研发人员,同时研发人员也可以快速定位到对应业务请求。
  • 在出现数据库死锁,阻塞时,可快速定位问题源头。
  • 在进行复杂的数据库运维操作时(如数据库迁移),能够与研发团队进行高效合作
  • (还有很多,欢迎大家分享自己的经验)

特别是在一些有着规模化研发团队的公司内,这种注释在运维工作中变得极为重要。一些互联网大厂甚至会在SQL规范中明确要求增加该注释。

在笔者所在的团队,虽然团队规模不大,但是面对公司业务不断增长的形势,对运维工作进行规范化是非常必要的。所以笔者开始在公司开发环境下,找寻可行的解决方案。

环境

  • springboot 2.6.11
  • mybatis-plus:3.5.3.1
  • mybatis:3.5.11

业务目标

  • 在所有SQL头部增加注释,格式:/* xxxxxxxxxxxxxxxxxxxxxxxx */
  • 注释中包含:
    • appUk(应用标识)
    • host ip (宿主机IP地址)
    • 请求追踪信息(brave追踪协议)

技术实现

实现这种技术目标的解决方案有多种。笔者在这个技术方案中,使用了MyBatis的拦截器接口org.apache.ibatis.plugin.Interceptor。关于拦截器的详细使用方法,可以参看《mybatis:自定义实现拦截器插件Interceptor》这篇文章。

拦截器实现代码如下:

package test.interceptor;

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 org.slf4j.MDC;

import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.Connection;

/**
 * MyBatisPlusSqlAnnotationInterceptor
 * <p>
 * 用于为MyBatisPlus的SQL语句添加注释,标记语句的执行者
 *
 * @author John Chen
 * @since 2023/5/6
 */
@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class}
        )
})
@Slf4j
public class MyBatisPlusSqlAnnotationInterceptor implements Interceptor {

    private final String appUk;

    private String ip = "UNKNOWN";

    public MyBatisPlusSqlAnnotationInterceptor(String appUk) {
        this.appUk = appUk;
        try {
            ip = InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("获取本机IP失败", e);
        }
    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        BoundSql boundSql = statementHandler.getBoundSql();
        String sql = boundSql.getSql();
        String annotation = String.format("/* **********appUk:[%s];host ip:[%s];traceId:[%s];spanId[%s]********** */",
                appUk, ip, MDC.get("traceId"), MDC.get("spanId"));
        // 在SQL语句前面加上注释
        sql = annotation + sql;
        // 用反射修改boundSql的sql属性
        Field field = boundSql.getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(boundSql, sql);
        return invocation.proceed();
    }
}

在完成拦截器的主体代码后,需要将它注入到MyBatis的SqlSessionFactory中。在笔者的项目中,这块代码是通过Java Config的方式实现的。

public class MySqlConfig {

    @Value("${spring.application.name}")
    private String appUk;
    
    /**
     * 定义一个统一增加注释的拦截器
     */
    @Bean
    public MyBatisPlusSqlAnnotationInterceptor myBatisPlusSqlAnnotationInterceptor() {
        return new MyBatisPlusSqlAnnotationInterceptor(appUk);
    }
    
    /**
     * 构建SqlSessionFactory
     * <p>
     * 注意,这里构建后会影响自动配置类{@link com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration#sqlSessionFactory(DataSource)}
     * 的构建。后期如果增加插件,请务参照对照方法在内部增加注入配置
     */
    @Bean
    public SqlSessionFactory testIwrsSqlSessionFactory(DataSource dataSource, GlobalConfig globalConfig, MybatisPlusInterceptor paginationInterceptor, MyBatisPlusSqlAnnotationInterceptor myBatisPlusSqlAnnotationInterceptor) throws Exception {
        final MybatisSqlSessionFactoryBean sqlSessionFactoryBean = new MybatisSqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setVfs(SpringBootVFS.class);
        sqlSessionFactoryBean.setMapperLocations(
                new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/test/**Mapper.xml")
        );
        sqlSessionFactoryBean.setTypeAliasesPackage("test.common.entity.*");
        sqlSessionFactoryBean.setGlobalConfig(globalConfig);
        sqlSessionFactoryBean.setConfiguration(defaultMybatisConfiguration());
		//**核心代码看这里**
        Interceptor[] plugins = new Interceptor[]{
                paginationInterceptor
                //自动为SQL增加注释的拦截器
                , myBatisPlusSqlAnnotationInterceptor
        };
        sqlSessionFactoryBean.setPlugins(plugins);
        return sqlSessionFactoryBean.getObject();
    }

通过如上2步的配置,就完成了统一增加注释的功能

Tips

  • annotation变量中的注释内容,应当根据公司规定和项目实际情况进行调整。目的是能够将SQL和具体的团队、业务、请求进行快速关联,以方便跨职能,甚至跨部门的技术协作。
  • 一些云数据库,可能会通过SQL前部注释的方式完成一些特定功能。如阿里云的PolarDB for AI、PolarDB-X等。这种情况下,增加额外的注释是否会导致具体的功能无法正常运行,需要再实际落地过程中进行验证。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术流奶爸奶爸

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值