大家好,我是 方圆。在本节我们来实现一个 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
, StatementHandler
和 Executor
处理器(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
)和"承接"处理结果集的逻辑,这里描述成"承接"的意思是这两部分职责并不是由它处理,而是分别由ParameterHandler
和ResultSetHandler
完成,所以拦截器切入它主要是 在准备和执行阶段对 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
中组合了 SqlCommand
和 MethodSignature
,SqlCommand
对象很重要,它的 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 的执行参数则由 参数处理器 DefaultParameterHandler
和 TypeHandler
完成,ResultSet
结果的处理:将数据库中数据转换成所需要的 Java 对象由 结果处理器 DefaultResultSetHandler
完成。
现在我们对拦截器的原理和查询 SQL 的执行流程已经有了基本的了解,回过头来再想一下我们的需求:"使用 Mybatis 的拦截器在 SQL 执行前进行打标",那么我们该选择哪个方法作为切入点更合适呢?
理论上来说在 Executor
, StatementHandler
和 ParameterHandler
相关的方法中切入都可以,但实际上我们还要多考虑一步: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 进行打标了,但在打标之前我们需要先思考下要打标的内容是什么:
- 要清楚的知道被执行的 SQL 是定义在 Mapper 中的哪条:声明在 Mapper 中各个方法的唯一ID,也就是 StatementId
- 要清楚的知道这条 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