由 Mybatis 源码畅谈软件设计(七):SQL “染色” 拦截器实战

大家好,我是 方圆。在本节我们来实现一个 SQL "染色" 的拦截器:即在 SQL 执行前对 SQL 进行打标(添加 StatementId 和线程方法堆栈注释),以快速、清楚的知道慢 SQL 和高并发执行 SQL 的方法调用链路。实现这样一个拦截器需要对 Mybatis 查询流程比较熟悉,所以借着本次案例实战的机会再来熟悉一下先前学习的源码内容。本文会先讲 "拦截器的作用范围",并重新回顾 SELECT 语句的执行流程,在彻底了解了拦截器的实现原理之后,再入手实现这个拦截器,最后验证拦截器的功能,希望大家对 Mybatis 源码有更好的理解。

拦截器的作用范围

Mybatis 的拦截器不像 Spring 的 AOP 机制,它并不能在任意逻辑处进行切入。在 Mybatis 源码的 Configuration 类中,定义了它的拦截器的作用范围,即创建"四大处理器"时调用的 pluginAll 方法:

java 复制代码
public class Configuration {
    // ...
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject,
                                                BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,
                parameterObject, boundSql);
        // 拦截器相关逻辑
        return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
    }

    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds,
                                                ParameterHandler parameterHandler, ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler,
                resultHandler, boundSql, rowBounds);
        // 拦截器相关逻辑
        return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
    }

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
                                                Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
                rowBounds, resultHandler, boundSql);
        // 拦截器相关逻辑
        return (StatementHandler) interceptorChain.pluginAll(statementHandler);
    }

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        // 创建具体的 Executor 实现类
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        // 拦截器相关逻辑
        return (Executor) interceptorChain.pluginAll(executor);
    }

}

pluginAll 是让拦截器生效的逻辑,它具体是如何做的呢:

java 复制代码
public class InterceptorChain {

    // 所有配置的拦截器
    private final List<Interceptor> interceptors = new ArrayList<>();

    public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
            // 注意 target 引用不断变化,会不断引用已经添加拦截器的对象
            target = interceptor.plugin(target);
        }
        return target;
    }

    // ...
}

InterceptorChain 实现非常简单,内部定义了集合来保存所有配置的拦截器,执行 pluginAll 方法时会遍历该集合,逐个调用 Interceptor#plugin 方法来 "不断地叠加拦截器"(interceptor.plugin 方法执行时,target 引用不断变更)。

注意这里使用到了 责任链模式 ,由 InterceptorChain 的命名中包含 Chain 也能联想到该模式,之后我们在使用责任链时也可以考虑在命名中增加 Chain 以增加可读性。InterceptorChain 将多个拦截器串联在一起,每个拦截器负责其特定的逻辑处理,并在执行完自己的逻辑后,调用下一个拦截器或目标方法,这样设计允许不同的拦截器之间的逻辑 解耦 ,同时提供了 可扩展性

由此可知,拦截器的作用范围是 ParameterHandler, ResultSetHandler, StatementHandlerExecutor 处理器(Handler),但是拦截它们又能实现什么效果呢?

要弄清楚这个问题,首先我们需要了解拦截器能够切入的粒度。在 Mybatis 框架中,定义拦截器时需要使用 @Intercepts@Signature 注解来 配置切入的方法,如下所示:

java 复制代码
@Intercepts({
        @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
@Service
public class SQLMarkingInterceptor implements Interceptor {
    // ...
}

每个拦截器切入的 粒度是方法级别的 的,比如在我们定义的这个拦截器中,切入的是 StatementHandler#prepare 方法,那么如果我们了解了四个处理器方法的作用是不是就能知道 Mybatis 拦截器所能实现的功能了?所以接下来我们简单了解一下它们的各个方法的作用:

  • ParameterHandler: 核心方法 setParameters,它的作用主要是将 Java 对象转换为 SQL 语句中的参数,并处理参数的设置和映射,所以拦截器切入它能 对 SQL 执行的入参进行修改
java 复制代码
public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}
  • ResultSetHandler: 负责将 SQL 查询返回的 ResultSet 结果集转换为 Java 对象,拦截器切入它的方法能 对结果集进行处理
java 复制代码
public interface ResultSetHandler {

  /**
   * 处理 Statement 对象并返回结果对象
   *
   * @param stmt SQL 语句执行后返回的 Statement 对象
   * @return 映射后的结果对象列表
   */
  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  /**
   * 处理 Statement 对象并返回一个 Cursor 对象
   * 它用于处理从数据库中获取的大量结果集,与传统的 List 或 Collection 不同,Cursor 提供了一种流式处理结果集的方式,
   * 这在处理大数据量时非常有用,因为它可以避免将所有数据加载到内存中
   *
   * @param stmt SQL 语句执行后返回的 Statement 对象
   * @return 游标对象,用于迭代结果集
   */
  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  /**
   * 处理存储过程的输出参数
   *
   * @param cs 存储过程调用的 CallableStatement 对象
   */
  void handleOutputParameters(CallableStatement cs) throws SQLException;

}
  • Executor: 它的方法很多,概括来说它负责数据库操作,包括增删改查等基本的 SQL 操作、管理缓存和事务的提交与回滚,所以拦截器切入它主要是 管理执行过程或事务
java 复制代码
public interface Executor {

    ResultHandler NO_RESULT_HANDLER = null;

    // 该方法用于执行更新操作(包括插入、更新和删除),它接受一个 `MappedStatement` 对象和更新参数,并返回受影响的行数
    int update(MappedStatement ms, Object parameter) throws SQLException;

    // 该方法用于执行查询操作,接受 `MappedStatement` 对象(包含 SQL 语句的映射信息)、查询参数、分页信息、结果处理器等,并返回查询结果的列表
    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
                      CacheKey cacheKey, BoundSql boundSql) throws SQLException;

    <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
            throws SQLException;

    <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

    // 该方法用于刷新批处理语句并返回批处理结果
    List<BatchResult> flushStatements() throws SQLException;

    // 该方法用于提交事务,参数 `required` 表示是否必须提交事务
    void commit(boolean required) throws SQLException;

    // 该方法用于回滚事务。参数 `required` 表示是否必须回滚事务
    void rollback(boolean required) throws SQLException;

    // 该方法用于创建缓存键,缓存键用于标识缓存中的唯一查询结果
    CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

    // 该方法用于检查某个查询结果是否已经缓存在本地
    boolean isCached(MappedStatement ms, CacheKey key);

    // 该方法用于清空一级缓存
    void clearLocalCache();

    // 该方法用于延迟加载属性
    void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

    // 该方法用于获取当前的事务对象
    Transaction getTransaction();

    // 该方法用于关闭执行器。参数 `forceRollback` 表示是否在关闭时强制回滚事务
    void close(boolean forceRollback);

    boolean isClosed();

    // 该方法用于设置执行器的包装器
    void setExecutorWrapper(Executor executor);

}
  • StatementHandler: 它的主要职责是准备(prepare)、"承接"封装 SQL 执行参数的逻辑,执行SQL(update/query)和"承接"处理结果集的逻辑,这里描述成"承接"的意思是这两部分职责并不是由它处理,而是分别由 ParameterHandlerResultSetHandler 完成,所以拦截器切入它主要是 在准备和执行阶段对 SQL 进行加工等
java 复制代码
public interface StatementHandler {

    Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException;

    void parameterize(Statement statement) throws SQLException;

    void batch(Statement statement) throws SQLException;

    int update(Statement statement) throws SQLException;

    <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException;

    <E> Cursor<E> queryCursor(Statement statement) throws SQLException;

    BoundSql getBoundSql();

    ParameterHandler getParameterHandler();

}

为了加深大家对这四个处理器的理解,了解它在查询 SQL 执行时作用的时机,我们来看一下查询 SQL 执行时的流程图:

每个声明 SQL 查询语句的 Mapper 接口都会被 MapperProxy 代理,接口中每个方法都会被定义为 MapperMethod 对象,借助 PlainMethodInvoker 执行(动态代理模式和策略模式),MapperMethod 中组合了 SqlCommandMethodSignatureSqlCommand 对象很重要,它的 SqlCommand#name 字段记录的是 MappedStatement 对象的 ID 值(eg: org.apache.ibatis.domain.blog.mappers.AuthorMapper.selectAuthor),根据它来获取唯一的 MappedStatement(每个 MappedStatement 对象对应 XML 映射文件中一个 <select>, <insert>, <update>, 或 <delete> 标签定义),SqlCommand#type 字段用来标记 SQL 的类型。当方法被执行时,会先调用 SqlSession 中的查询方法 DefaultSqlSession#selectOne,接着由 执行器 Executor 去承接,默认类型是 CachingExecutor,注意在这里它会调用 MappedStatement#getBoundSql 方法获取 BoundSql 对象,这个对象实际上最终都是在 StaticSqlSource#getBoundSql 方法中获取的,也就是说 此时我们定义在 Mapper 文件中的 SQL 此时已经被解析、处理好了(动态标签等内容均已被处理) ,保存在了 BoundSql 对象中。此时,要执行的 SQL 已经准备好了,它会接着调用 SQL 处理器StatementHandler#prepare 方法创建与数据库交互的 Statement 对象,其中记录了要执行的 SQL 信息 ,而封装 SQL 的执行参数则由 参数处理器 DefaultParameterHandlerTypeHandler 完成,ResultSet 结果的处理:将数据库中数据转换成所需要的 Java 对象由 结果处理器 DefaultResultSetHandler 完成。

现在我们对拦截器的原理和查询 SQL 的执行流程已经有了基本的了解,回过头来再想一下我们的需求:"使用 Mybatis 的拦截器在 SQL 执行前进行打标",那么我们该选择哪个方法作为切入点更合适呢?

理论上来说在 Executor, StatementHandlerParameterHandler 相关的方法中切入都可以,但实际上我们还要多考虑一步:ParameterHandler 是用来处理参数相关的,在这里切入一般我们是要对入参 SQL 的入参进行处理,所以不选择这里避免为后续同学维护时增加理解成本;Executor "有时不是很合适",它其中有两个 query 方法,先被执行的方法,对应图中 CacheExecutor 左侧的直线 query 方法Executor#query(MappedStatement, Object, RowBounds, ResultHandler),在方法中它会去调用 MappedStatement#getBoundSql 方法获取 BoundSql 对象 完成 SQL 的处理和解析 ,处理和解析后的 BoundSql 对象是我们需要进行拦截处理的,随后 在该方法内部 调用另一个 query 方法:Executor#query(MappedStatement, Object, RowBounds, ResultHandler, CacheKey, BoundSql)对应图中 CacheExecutor 右侧的曲线 query 方法 ,它会将 BoundSql 作为入参去执行查询逻辑,结合本次需求,选择后者切入是合适的,因为它有 BoundSql 入参,对这个入参进行打标即可,我们来看一下 CachingExecutor 的源码:

java 复制代码
public class CachingExecutor implements Executor {

    private final Executor delegate;
    
    // 先调用
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler)
            throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameterObject);
        CacheKey key = createCacheKey(ms, parameterObject, rowBounds, boundSql);
        // 在方法内部调用
        return query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }

    @Override
    public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler,
                             CacheKey key, BoundSql boundSql) throws SQLException {
        // 二级缓存相关逻辑
        Cache cache = ms.getCache();
        if (cache != null) {
            flushCacheIfRequired(ms);
            if (ms.isUseCache() && resultHandler == null) {
                ensureNoOutParams(ms, boundSql);
                @SuppressWarnings("unchecked")
                List<E> list = (List<E>) tcm.getObject(cache, key);
                if (list == null) {
                    // 执行查询逻辑(被拦截)
                    list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
                    tcm.putObject(cache, key, list);
                }
                return list;
            }
        }
        // 执行查询逻辑(被拦截)
        return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
    }
}

它使用了静态代理模式,其中封装的 Executor 实现类型为 SimpleExecutor,在注释中标记了"被拦截"处的方法会让拦截器生效。那么前文中为什么要说它"有时不是很合适"呢?我们来看一种情况,将 Mybatis 配置中的 cacheEnable 置为 false,那么在创建执行器时实际类型不是 CachingExecutor 而是 SimpleExecutor,如下源码所示:

java 复制代码
public class Configuration {

    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        // 创建具体的 Executor 实现类
        Executor executor;
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }
        // false 不走这段逻辑
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        // 拦截器相关逻辑
        return (Executor) interceptorChain.pluginAll(executor);
    }
}

当有 SELECT 查询语句被执行时,它会直接调用到 BaseExecutor#query 方法中,在方法内部调用另一个需要被拦截的 query 方法,如下所示:

java 复制代码
public abstract class BaseExecutor implements Executor {
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler)
            throws SQLException {
        BoundSql boundSql = ms.getBoundSql(parameter);
        // cache key 缓存操作
        CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
        // 需要拦截的
        return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
    }

    @SuppressWarnings("unchecked")
    @Override
    public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
                             CacheKey key, BoundSql boundSql) throws SQLException {
        // ...
    }
}

由于该方法是在方法内部被调用的,所以无法使拦截器生效 (动态代理),这也是说它"有时不是很合适"的原因所在。因为存在这种情况,我们现在也只能选择 StatementHandler 作为切入点了,那么是选择切入 StatementHandler#prepare 方法还是 StatementHandler#query 方法呢?

java 复制代码
public class SimpleExecutor extends BaseExecutor {
    public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler,
                               BoundSql boundSql) throws SQLException {
        Statement stmt = null;
        try {
            Configuration configuration = ms.getConfiguration();
            // 创建 StatementHandler
            StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler,
                    boundSql);
            // 准备 Statement,其中会调用 StatementHandler#prepare 方法
            stmt = prepareStatement(handler, ms.getStatementLog());
            // 由 StatementHandler 执行 query 方法
            return handler.query(stmt, resultHandler);
        } finally {
            closeStatement(stmt);
        }
    }
}

根据源码,要被执行打标的 BoundSql 对象会在调用 StatementHandler#prepare 方法前会将 BoundSql 对象封装在 StatementHandler 中,如果选择切入 StatementHandler#prepare 方法,那么在该方法执行前在 StatementHandler 中拿到 BoundSql 对象进行修改便能实现我们的需求;如果选择切入 StatementHandler#query 方法,同样是需要在该方法执行前想办法获取到 BoundSql 对象,但是由于此时 SQL 信息已经被保存在了即将与数据库交互的 Statement 对象中,它的实现类有很多,比如常见的 PreparedStatement,在其中获取 SQL 字符串相对复杂,所有还是选择 StatementHandler#prepare 方法作为切点相对容易。

拦截器的定义和源码解析

接下来我们来对拦截器进行实现,首先我们先对拦截器的切入点进行定义:

java 复制代码
@Intercepts({
        @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
public class SQLMarkingInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // ...
    }
}

接着来实现其中的逻辑:

java 复制代码
@Intercepts({
        @Signature(method = "prepare", type = StatementHandler.class, args = {Connection.class, Integer.class})
})
public class SQLMarkingInterceptor implements Interceptor {

    private static final Log log = LogFactory.getLog(SQLMarkingInterceptor.class);
    
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            // 1. 找到 StatementHandler(SQL 执行时,StatementHandler 的实际类型为 RoutingStatementHandler)
            RoutingStatementHandler routingStatementHandler = getRoutingStatementHandler(invocation.getTarget());

            if (routingStatementHandler != null) {
                // 其中 delegate 是实际类型的 StatementHandler (静态代理模式),获取到实际的 StatementHandler
                StatementHandler delegate = getFieldValue(
                        RoutingStatementHandler.class, routingStatementHandler, "delegate", StatementHandler.class
                );
                // 2. 找到 StatementHandler 之后便能拿到 SQL 相关信息,现在对 SQL 信息打标即可
                marking(delegate);
            }
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }

        return invocation.proceed();
    }
}

将自定义的逻辑添加上了 try-catch,避免异常影响正常业务的执行。在主要逻辑中,需要先在 Invocation 中找到 StatementHandler 的实际被代理的对象,它被封装在了 RoutingStatementHandler 中,随后在 StatementHandler 中获取到 BoundSql 对象,对 SQL 进行打标即可(marking 方法)。

获取 StatementHandler

拦截 StatementHandler 为什么要获取的是 RoutingStatementHandler 类型呢?我们回到拦截器拦截 StatementHandler 生效的源码:

java 复制代码
public class Configuration {
    // ...
    protected final InterceptorChain interceptorChain = new InterceptorChain();

    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
                                                Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        // 可以发现拦截器实际针对的是类型便是 RoutingStatementHandler
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject,
                rowBounds, resultHandler, boundSql);
        // 拦截器相关逻辑
        return (StatementHandler) interceptorChain.pluginAll(statementHandler);
    }
    
}

我们可以发现拦截器在生效时,针对的是 RoutingStatementHandler 类型,所以我们要获取该类型,如下源码:

java 复制代码
public class SQLMarkingInterceptor implements Interceptor {
    
    private RoutingStatementHandler getRoutingStatementHandler(Object target)
            throws NoSuchFieldException, IllegalAccessException {
        // 如果被代理,那么一直找到具体被代理的对象
        while (Proxy.isProxyClass(target.getClass())) {
            target = Proxy.getInvocationHandler(target);
        }
        while (target instanceof Plugin) {
            Plugin plugin = (Plugin) target;
            target = getFieldValue(Plugin.class, plugin, "target", Object.class);
        }
        // 找到了 RoutingStatementHandler
        if (target instanceof RoutingStatementHandler) {
            return (RoutingStatementHandler) target;
        }

        return null;
    }
    
}

源码中前两步为处理代理关系的逻辑,因为 RoutingStatementHandler 可能被代理,需要获取到实际的被代理对象,找到之后返回即可。那么后续为什么还要获取到 RoutingStatementHandler 中的被代理对象呢?我们还需要再回到 Mybatis 的源码中:

java 复制代码
public class RoutingStatementHandler implements StatementHandler {

    // 代理对象
    private final StatementHandler delegate;

    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds,
                                   ResultHandler resultHandler, BoundSql boundSql) {
        // 在调用构造方法时,根据 statementType 字段为代理对象 delegate 赋值,那么这样便实现了复杂度隐藏,只由代理对象去帮忙路由具体的实现即可
        switch (ms.getStatementType()) {
            case STATEMENT:
                delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case PREPARED:
                delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case CALLABLE:
                delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            default:
                throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
        }

    }
}

RoutingStatementHandler 使用了静态代理模式,实际的类型被赋值到了 delegate 字段中,我们需要在这个对象中获取到 BoundSql 对象,获取 delegate 对象则通过反射来完成。

打标 marking

现在我们已经获取到了 StatementHandler delegate 对象,我们可以 SQL 进行打标了,但在打标之前我们需要先思考下要打标的内容是什么:

  1. 要清楚的知道被执行的 SQL 是定义在 Mapper 中的哪条:声明在 Mapper 中各个方法的唯一ID,也就是 StatementId
  2. 要清楚的知道这条 SQL 被执行时,有哪些相关方法被执行了:方法的调用栈

根据我们所需去找相关的内容就好了,以下是源码,需要注意的是由于所有类型的 SQL 都会执行到 prepare 方法,但我们只对 SELECT 语句进行打标,所以需要添加逻辑判断:

java 复制代码
public class SQLMarkingInterceptor implements Interceptor {
    
    private void marking(StatementHandler delegate) throws NoSuchFieldException, IllegalAccessException {
        BoundSql boundSql = delegate.getBoundSql();
        // 实际的 SQL
        String sql = boundSql.getSql().trim();
        // 只对 select 打标
        if (StringUtils.containsIgnoreCase(sql, "select")) {
            // 获取其基类中的 MappedStatement 即定义的 SQL 声明对象,获取它的 ID 值表示它是哪条 SQL
            MappedStatement mappedStatement = getFieldValue(
                    BaseStatementHandler.class, delegate, "mappedStatement", MappedStatement.class
            );
            String mappedStatementId = mappedStatement.getId();
            // 方法调用栈
            String trace = trace();

            // 按顺序创建打标的内容
            LinkedHashMap<String, Object> markingMap = new LinkedHashMap<>();
            markingMap.put("STATEMENT_ID", mappedStatementId);
            markingMap.put("STACK_TRACE", trace);
            String marking = "[SQLMarking] ".concat(markingMap.toString());

            // 打标
            sql = String.format(" /* %s */ %s", marking, sql);

            // 反射更新
            Field field = getField(BoundSql.class, "sql");
            field.set(boundSql, sql);
        }
    }
    
}

执行打标的逻辑是修改 BoundSql 对象,将其中的 sql 字段用打标后的 SQL 替换掉。获取方法调用栈的逻辑我们具体来看一下,其实并不复杂,在全量堆栈信息中将不需要关注的堆栈排除掉,需要注意将 !className.startsWith("com.your.package") 修改成有效的路径判断:

java 复制代码
public class SQLMarkingInterceptor implements Interceptor {
    
    private String trace() {
        // 全量调用栈
        StackTraceElement[] stackTraceArray = Thread.currentThread().getStackTrace();
        if (stackTraceArray.length <= 2) {
            return EMPTY;
        }
        LinkedList<String> methodInfoList = new LinkedList<>();
        for (int i = stackTraceArray.length - 1 - DEFAULT_INDEX; i >= DEFAULT_INDEX; i--) {
            StackTraceElement stackTraceElement = stackTraceArray[i];
            // 排除掉不想看到的内容
            String className = stackTraceElement.getClassName();
            if (!className.startsWith("com.your.package") || className.contains("FastClassBySpringCGLIB")
                    || className.contains("EnhancerBySpringCGLIB") || stackTraceElement.getMethodName().contains("lambda$")
            ) {
                continue;
            }
            // 过滤拦截器相关
            if (className.contains("Interceptor") || className.contains("Aspect")) {
                continue;
            }

            // 只拼接类和方法,不拼接文件名和行号
            String methodInfo = String.format("%s#%s",
                    className.substring(className.lastIndexOf('.') + 1),
                    stackTraceElement.getMethodName()
            );
            methodInfoList.add(methodInfo);
        }

        if (methodInfoList.isEmpty()) {
            return EMPTY;
        }

        // 格式化结果
        StringJoiner stringJoiner = new StringJoiner(" ==> ");
        for (String method : methodInfoList) {
            stringJoiner.add(method);
        }
        return stringJoiner.toString();
    }
    
}

以上便完成了 SQL "染色" 拦截器的实现,将其添加到 mybatis 相关的拦截器配置中就可以生效了。后续执行的 SELECT 语句如下所示,经过性能验证打标操作耗时 0~1ms 左右:

text 复制代码
 /* [SQLMarking] {STATEMENT_ID=OrdersMapper.selectList, STACK_TRACE=OrderResourceImpl#queryOrderList ==> OrdersServiceImpl#queryByParams ==> OrdersManagerImpl#queryByParams} */ SELECT * FROM orders WHERE (order_id = ?) LIMIT 100
相关推荐
全栈派森36 分钟前
云存储最佳实践
后端·python·程序人生·flask
CircleMouse41 分钟前
基于 RedisTemplate 的分页缓存设计
java·开发语言·后端·spring·缓存
獨枭2 小时前
使用 163 邮箱实现 Spring Boot 邮箱验证码登录
java·spring boot·后端
维基框架2 小时前
Spring Boot 封装 MinIO 工具
java·spring boot·后端
秋野酱2 小时前
基于javaweb的SpringBoot酒店管理系统设计与实现(源码+文档+部署讲解)
java·spring boot·后端
☞无能盖世♛逞何英雄☜2 小时前
Flask框架搭建
后端·python·flask
进击的雷神2 小时前
Perl语言深度考查:从文本处理到正则表达式的全面掌握
开发语言·后端·scala
进击的雷神3 小时前
Perl测试起步:从零到精通的完整指南
开发语言·后端·scala
豌豆花下猫4 小时前
Python 潮流周刊#102:微软裁员 Faster CPython 团队(摘要)
后端·python·ai
秋野酱4 小时前
基于javaweb的SpringBoot驾校预约学习系统设计与实现(源码+文档+部署讲解)
spring boot·后端·学习