Plugin解析

要讲清楚 MyBatis 插件的加载与生效,最好从两个时间点切入:一是“启动期”的加载,二是“运行时”的织入,因此本章我将通过介绍这两个过程,让大家熟悉 MyBatis 的插件原理。

一、Plugin 加载过程

插件的加载始于 MyBatis 容器的初始化。当 XMLConfigBuilder解析到配置中的 <plugins>标签时,便开始逐步构建插件链。

对于每一个 <plugin>子标签,MyBatis 都会触发对应拦截器的实例化流程:

  1. 先通过无参构造函数创建 Interceptor对象。

  2. 再自动调用其 setProperties()方法,以传入配置参数,完成拦截器的个性化设置。

所有初始化完成的拦截器实例,最终被统一注册Configuration对象的 interceptorChain中 —— 该成员变量实际上是一个 ArrayList<Interceptor>列表,维护着整个插件链的执行顺序。

加载时序图

源码追踪

  • 起点SqlSessionFactoryBuilder.build(...)调用解析

    // 核心代码段
    XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
    return build(parser.parse());

    解读:创建 XMLConfigBuilder,随后立即调用 parse()方法。所有“启动期”的加载工作都发生在 parse()内。

  • 解析入口XMLConfigBuilder.parse()

    // 核心代码片段
    parseConfiguration(parser.evalNode("/configuration"));

    解读:它锁定根节点 /configuration并进入逐段解析。

  • 关键步骤:进入 parseConfiguration(...)并处理 <plugins>

    // 核心代码片段
    pluginsElement(root.evalNode("plugins"));

    解读:按照固定顺序解析。注意 <plugins><settings>之后、<mappers>之前。这个顺序背后有因果:插件需要在后续组件创建前“先被登记”。

  • 插件装载pluginsElement(...)

    // 核心代码片段
    for (XNode child : context.getChildren()) {
        String interceptor = child.getStringAttribute("interceptor");
        Properties properties = child.getChildrenAsProperties();
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
    }

    解读:逐个读取 <plugin>节点,获取 interceptor类、构造实例、注入 <property>属性,然后将其添加到 Configuration中。

    说明这一步只做“登记”,不做“织入”。它把插件排进一条链,后续对象创建时会统一查询这条链来决定是否进行包装。

  • 注册到配置Configuration.addInterceptor(...)

    // 核心代码片段
    public void addInterceptor(Interceptor interceptor) {
        interceptorChain.addInterceptor(interceptor);
    }

    解读Configuration不直接维护列表,而是把工作委托给 InterceptorChain

    说明:把“登记”和“应用”分离到两个阶段、两个类里,利于演化。Configuration保持“高层入口”,InterceptorChain负责“具体集合与调度”。

  • 插件链InterceptorChain

    // 核心代码片段
    private final List<Interceptor> interceptors = new ArrayList<>();
    public void addInterceptor(Interceptor interceptor) {
        interceptors.add(interceptor);
    }

    解读:一个简单的列表,按出现顺序保存插件。顺序很关键,因为将来应用插件时是按此顺序包装目标对象。

    说明:链式结构让插件具备“组合”的能力,每个插件都能在前一个的包装结果上继续包裹,形成 AOP 风格的层叠。


二、Plugin 织入过程

这个过程主要基于动态代理​ + 责任链模式,实现了对核心组件的层层封装,也常被形象地称为 “洋葱模型”

当框架初始化并创建四大核心组件(例如 Executor)时,包装流程随即启动。在 Configuration的各个 newXXX()方法中,每生成一个核心对象,都会立即调用 interceptorChain.pluginAll(target)方法,给这个对象做一次“全身包装”。

这个过程如同为洋葱逐层裹上外衣——每个插件都可能为目标对象创建一个代理层,最终形成一个由多个代理嵌套而成的调用链。

织入时序图

源码追踪

1. Executor
// 核心代码片段
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    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);
}
2. StatementHandler
// 核心代码片段
// org/apache/ibatis/executor/SimpleExecutor.java:45-54
StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, null, null);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.update(stmt);

// org/apache/ibatis/executor/SimpleExecutor.java:60-69
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
stmt = prepareStatement(handler, ms.getStatementLog());
return handler.query(stmt, resultHandler);

// org/apache/ibatis/session/Configuration.java:724-729
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
return (StatementHandler) interceptorChain.pluginAll(statementHandler);

// org/apache/ibatis/executor/statement/RoutingStatementHandler.java:40-56
switch (ms.getStatementType()) {
    case STATEMENT:  delegate = new SimpleStatementHandler(...); break;
    case PREPARED:   delegate = new PreparedStatementHandler(...); break;
    case CALLABLE:   delegate = new CallableStatementHandler(...); break;
    default: throw new ExecutorException(...);
}

// org/apache/ibatis/executor/statement/BaseStatementHandler.java:53-73
this.parameterHandler = configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
this.resultSetHandler = configuration.newResultSetHandler(executor, mappedStatement, rowBounds, parameterHandler, resultHandler, boundSql);

解读

  • 构建 RoutingStatementHandler时,具体子类继承自 BaseStatementHandler。进入父类构造器后立刻创建 ParameterHandlerResultSetHandler,且两者都是通过 Configuration各自的 pluginAll包裹完成。

  • 待具体 StatementHandler子类构造完成,Configuration.newStatementHandler再对整个 StatementHandler本体做一次 pluginAll

  • 顺序是ParameterHandler wrapResultSetHandler wrapStatementHandler wrap

3. ParameterHandler
// 核心代码片段
// org/apache/ibatis/session/Configuration.java:710-715
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
return (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
// 来源调用:org/apache/ibatis/executor/statement/BaseStatementHandler.java:66-73

解读:由 LanguageDriver基于语句语言(XML/注解)创建后,立刻通过 pluginAll包裹,典型拦截点是 setParameters(PreparedStatement)。被 BaseStatementHandler构造器调用并注入到语句处理器内部。

4. ResultSetHandler
// 核心代码片段
// org/apache/ibatis/session/Configuration.java:717-722
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
return (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
// 来源调用:org/apache/ibatis/executor/statement/BaseStatementHandler.java:70-73

解读:直接构造 DefaultResultSetHandler,随后统一 pluginAll包裹,拦截点集中在 handleResultSets(Statement)handleCursorResultSets(Statement)。注入时携带已包裹的 parameterHandler,使得前后两个环节的插件增强能够协同。

pluginAll 原理

// 核心代码片段
// org/apache/ibatis/plugin/InterceptorChain.java:29-33
for (Interceptor interceptor : interceptors) {
    target = interceptor.plugin(target);
}

// org/apache/ibatis/plugin/Interceptor.java:20-24
default Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

// org/apache/ibatis/plugin/Plugin.java:43-51
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
    return Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap));
}
return target;

// org/apache/ibatis/plugin/Plugin.java:53-64
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
if (methods != null && methods.contains(method)) {
    return interceptor.intercept(new Invocation(target, method, args));
}
return method.invoke(target, args);

// org/apache/ibatis/plugin/Invocation.java:60-63
public Object proceed() {
    return method.invoke(target, args);
}

解读

  • pluginAll会逐个调用拦截器的 plugin方法。默认实现通过 Plugin.wrap检查该拦截器的 @Intercepts/@Signature是否命中目标对象所实现的接口;命中则生成“只实现必要接口”的 JDK 动态代理。

  • 进入代理后,根据签名匹配决定是否拦截;命中则执行 interceptor.intercept(Invocation),调用 Invocation.proceed()继续执行原方法。


总结

总的来说,MyBatis 插件的核心原理可概括为 动态代理”与“责任链”的巧妙结合

  1. 启动时,所有声明的插件(Interceptor)都会被注册到一个统一的拦截器链中。

  2. 创建核心对象时,会通过 ConfigurationpluginAll()方法,基于动态代理技术,按声明顺序依次将每个插件“包裹”在目标对象外部,形成类似“洋葱模型”的链式结构。

  3. 运行时,方法调用会从最外层代理逐层向内传递。在调用链的每一个环节,插件都可以通过 @Signature匹配并拦截特定方法,执行自定义逻辑(如修改 SQL、记录日志等),再通过 invocation.proceed()将调用传递给链中的下一个代理或最终的真实对象。

这种设计使得插件能够以无侵入的方式,灵活地切入 MyBatis 执行流程的各个环节。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值