当前位置: 首页 > article >正文

通过实现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等。这种情况下,增加额外的注释是否会导致具体的功能无法正常运行,需要再实际落地过程中进行验证。

http://www.kler.cn/news/18504.html

相关文章:

  • 数据库基础操作 all in one
  • TCP四次挥手
  • Linux网络——Shell编程之数组
  • 天猫数据分析:2023年Q1天猫净水器品牌销售TOP10排行榜
  • 3. SQL底层执行原理详解
  • MVC分部视图的使用:Html.Partial/RenderPartial,Html.Action/RenderAction,RenderPage
  • 硬盘数据突然消失怎么回事?硬盘数据突然消失怎么找回
  • 【运动规划算法项目实战】八叉树地图(附ROS C++代码)
  • 如何用100天彻底学会Python?
  • JavaScript class和继承的原理
  • 【Queue新技法】用双数组实现一个队列 C++
  • C++类和对象(上)
  • 华为OD机试真题 Java 实现【猜字谜】【2023Q2】
  • Adobe考试
  • 【MySQL】索引
  • 字节跳动发放年终奖,远超预期~
  • 将sublime中的自定义代码片段snippet 转为vscode可用的代码片段 (cursor可用)
  • Java笔记_17(异常、File)
  • uboot 启动内核代码分析
  • C++结构体分别在:栈空间、堆空间、静态存储区中初始化
  • 【计算机专业漫谈】【计算机系统基础学习笔记】W2-2-1 原码和移码表示
  • vue概述
  • Go数据机构----栈与队列
  • CANoe以太网配置 Network-Based Access Mode
  • 离散化(算法)
  • 卫星下行链路预算模型(未完待续)
  • JavaScript (七) -- JavaScript 事件(需要了解的事件的运用)
  • C++运算符重载
  • 可视化绘图技巧100篇分析篇(二)-生存曲线(LM曲线)(补充篇)
  • EMC VNX登录Unisphere错误 certificate has invalid date问题处理