本文 的 原文 地址
原始的内容,请参考 本文 的 原文 地址
尼恩说在前面
在45岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如得物、阿里、滴滴、极兔、有赞、希音、百度、网易、美团、蚂蚁、得物的面试资格,遇到很多很重要的相关面试题:
- MyBatis插件是基于什么机制实现的?
- MyBatis插件中的责任链模式是如何实现的?与传统的责任链模式有何不同?
- 请解释MyBatis插件机制中, 如何体现 OCP 原则 和SRP 原则?
MyBatis 底层原理是常见的面试题。
最近有小伙伴在面 蚂蚁,又被问到了,可以说是逢面必问。
小伙伴没有系统的去梳理和总结,所以支支吾吾的说了几句,面试官不满意,面试挂了。
这里,尼恩给大家做一下系统化、体系化的梳理,使得大家内力猛增.
按照尼恩的思路做 暴击, 可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”,然后实现”offer直提”。
最终,机会爆表,实现”offer自由” 。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典PDF》V175版本,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
本文作者:
- 第一作者 老架构师 肖恩(肖恩 是尼恩团队 高级架构师,负责写此文的第一稿,初稿 )
- 第二作者 老架构师 尼恩 (45岁老架构师, 负责 提升此文的 技术高度,让大家有一种 俯视 技术、俯瞰技术、 技术自由 的感觉)

一: MyBatis 插件的应用 场景
MyBatis 插件的核心价值是 分离通用逻辑与业务逻辑,解决以下高频场景痛点:
-
SQL 监控与诊断:
统计 SQL 执行耗时、记录 SQL 语句及参数、检测慢 SQL 并报警;
-
通用功能封装:
分页逻辑(如 PageHelper 插件)、多租户数据隔离(自动添加租户 ID 条件)、数据权限控制(基于用户角色过滤数据);
-
数据安全处理:
敏感字段加密(如密码、手机号存入数据库时加密)、敏感字段解密(查询后自动解密展示);
-
日志增强:
定制化 SQL 日志格式(如添加请求 ID、用户 ID 便于链路追踪);
-
数据自动填充:
创建时间(createTime)、更新时间(updateTime)、创建人(creator)等公共字段的自动赋值;
-
SQL 改写:
动态添加查询条件、替换表名(如分表场景)、优化 SQL 语句(如添加索引提示)。
二: MyBatis 插件机制 基础使用: 从 0 到 1 入门
MyBatis 插件 不是 Maven 插件,也不是 IDEA 插件,而是 MyBatis 运行时动态代理链的一环。
MyBatis 插件 是基于 JDK 动态代理实现的一环,嵌入在 MyBatis 核心执行流程中的 运行时拦截链。
通过这一机制,开发者可以在不修改源码的前提下,动态地介入 SQL 执行过程,实现诸如性能监控、SQL 改写、数据加密等横切关注点功能。
MyBatis 插件 与 Spring AOP 中的“环绕通知”非常相似,但在 MyBatis 的上下文中更加轻量且专注。
2.1 Interceptor 定义
MyBatis 插件 官方文档英文叫 Interceptor,中文文档常译“拦截器”,但社区口语也喊“插件”。
MyBatis 插件 的实现类,必须实现 org.apache.ibatis.plugin.Interceptor 接口。
Interceptor 接口 核心方法包括:
-
intercept(Invocation invocation):拦截逻辑的核心实现方法;可以在此添加前置处理、后置处理,甚至完全替换原行为。
-
plugin(Object target):生成代理对象(默认调用 Plugin.wrap() 即可); 完成 JDK 动态代理包装。
-
setProperties(Properties properties):读取插件配置的属性(如分页 大小、阈值、加密密钥)。这个方法, 在插件初始化阶段被调用一次, 做配置解析和初始化工作。
2.2 MyBatis 插件 “插”在 哪儿
MyBatis 四大金刚接口的全部公有方法均可被拦截:
| 接口 | 典型可拦截方法 | 场景举例 |
|---|---|---|
| Executor | update, query, commit, rollback, close | 慢 SQL、读写分离、多租户 |
| StatementHandler | prepare, parameterize, batch, update, query | 分页改写 SQL、加 Hint |
| ParameterHandler | setParameters | 统一加密、脱敏 |
| ResultSetHandler | handleResultSets, handleOutputParameters | 解密、字典翻译、驼峰下划线 |
MyBatis 四大金刚接口 介绍如下:
- Executor 是 SQL 执行的总调度者,负责事务管理和缓存协调;
- StatementHandler 负责真正与 JDBC Statement 打交道,准备预编译语句;
- ParameterHandler 处理参数绑定,将 Java 对象映射到 PreparedStatement 的占位符;
- ResultSetHandler 则负责结果集的解析与映射。
MyBatis 四大金刚接口 贯穿了完整的 SQL 生命周期,因此它们成为插件最理想的切入点。例如:
-
如果 要做通用分页,最佳位置是拦截 Executor.query;
-
如果 想统一处理敏感字段加密,则应在 ParameterHandler.setParameters 阶段介入。
2.3 自定义 LimitPlugin 插件 示例
下面是一个典型的 MyBatis 插件实现,用于在 SQL 准备阶段, 插入日志输出 :
@Intercepts(
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}))
public class LimitPlugin implements Interceptor {
public Object intercept(Invocation inv) throws Throwable {
System.out.println("→ 准备阶段被拦截");
return inv.proceed(); // 必须调用,否则 SQL 不会继续
}
......
}
在这个例子中, 通过 @Intercepts 注解声明了要拦截的目标:StatementHandler.prepare(Connection, Integer) 方法。
当 MyBatis 执行任何 Mapper 查询时,只要经过该方法,就会触发 LimitPlugin.intercept() 回调。
上面的 注解 说明
MyBatis 使用 @Intercepts 和 @Signature 注解来声明插件的拦截点。
@Intercepts 和 @Signature 注解 其匹配机制极为严格,依赖于 类型、方法名、参数列表三者完全一致。
@Intercepts({
@Signature(
type = Executor.class, // 四大核心接口之一
method = "query", // 方法名(区分大小写)
args = {
MappedStatement.class,
Object.class,
RowBounds.class,
ResultHandler.class
} // 参数类型顺序必须完全一致
)
})
常见错误: 如 args 漏写 RowBounds, 启动不报错,但永远匹配不到。
LimitPlugin 完整的 代码如下:
@Intercepts(
@Signature(type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}))
public class LimitPlugin implements Interceptor {
public Object intercept(Invocation inv) throws Throwable {
System.out.println("→ 准备阶段被拦截");
return inv.proceed(); // 必须调用,否则 SQL 不会继续
}
/**
* 生成代理对象 - Interceptor 有默认实现,但可以自定义
*/
@Override
public Object plugin(Object target) {
// Interceptor 有默认实现 , 默认 实现通常就足够了
return Interceptor.super.plugin(target);
// 等价于:return Plugin.wrap(target, this);
}
/**
* 读取配置属性 -Interceptor 有默认实现, 可选实现
*/
@Override
public void setProperties(Properties properties) {
this.defaultLimit = Integer.parseInt(properties.getProperty("defaultLimit", "1000"));
this.enableLog = Boolean.parseBoolean(properties.getProperty("enableLog", "true"));
}
自定义 插件 注册:
为了让插件生效,需要将其注册到 MyBatis 的配置体系中。
最常见的方式是在 mybatis-config.xml 中使用 标签:
<plugins>
<plugin interceptor="demo.LimitPlugin"/>
</plugins>
运行任意 Mapper,控制台即可看到“→ 准备阶段被拦截”。
2.5 自定义 插件 配置方式大全
插件需注册到 MyBatis 中才能生效,支持 3 种注册方式:
传统 XML 配置方式(适用于纯 MyBatis 或早期 Spring 集成项目):
<bean class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="plugins">
<array><bean class="demo.LimitPlugin"/></array>
</property>
</bean>
这种方式通过 Spring Bean 容器管理插件实例,适合已有 XML 配置体系的老项目迁移。
Spring Boot 自动配置注册(推荐用于 Spring Boot 项目):
@Configuration
public class MyBatisConfig {
@Bean
public LimitPlugin limitPlugin() { // 方法名即 beanName
return new LimitPlugin();
}
}
利用 Spring 的自动装配机制,只需将插件声明为 Bean,MyBatis-Spring-Boot-Starter 会在启动时自动将其加入 Configuration.plugins 列表。
这是目前最简洁、最符合现代开发习惯的方式。
Java 代码配置注册(适用于无 XML 的轻量级应用或测试场景):
new SqlSessionFactoryBuilder()
.build(configuration)
.getConfiguration()
.addInterceptor(new LimitPlugin());
这种方式完全绕开配置文件,通过编程方式手动添加插件,灵活性最高,常用于单元测试或嵌入式场景。
三种方式本质相同,最终都是将 Interceptor 实例添加到 Configuration 的插件列表中。
选择哪种方式取决于项目的整体架构和技术演进路线。
2.6 自定义 插件 生命周期
自定义 插件 生命周期 的生与死:
-
实例化:MyBatis 启动时一次性创建 单例,随后一直复用。MyBatis 插件在容器启动阶段完成实例化,且全局唯一、单例存在。 无论有多少个 Mapper 或 SQL 执行请求,同一个插件类在整个 JVM 中只会被创建一次。
-
销毁:与 SqlSessionFactory 生命周期相同,几乎无需关心。
2.7 常见的MyBatis插件
| 名称 | 作用 | 备注 |
|---|---|---|
| MyBatis-Plus 分页插件 | 物理分页 | 基于 Executor.query |
| PageHelper | 物理分页 | 基于 Executor.query |
| MyBatis-Flex 审计插件 | 自动填充时间、操作人 | 基于 Executor.update |
| shardingsphere-jdbc | 分库分表 | 包装 Executor |
| mybatis-encrypt | 字段加解密 | 基于 ParameterHandler+ResultSetHandler |
这些插件的背后,其实都是对 Interceptor 接口的巧妙运用。例如:
- 分页插件在 Executor.query 阶段分析 SQL 并注入 LIMIT;
- 审计插件在 Executor.update 时检查实体字段并自动赋值;
- 加密插件则分别在参数设置和结果映射两个阶段完成加解密闭环。
注意事项与最佳实践
-
拦截点选择原则:按需选择最细粒度的拦截点(如仅需处理参数时,优先拦截 ParameterHandler. setParameters,而非 Executor.query),减少性能损耗;
-
插件执行顺序:多个插件的执行顺序与注册顺序一致(先注册的先执行),若需调整顺序,可通过配置文件或代码注册顺序调整;
-
无状态设计:插件应设计为无状态(不存储局部变量),避免多线程环境下的数据安全问题; 避免在插件中维护局部状态,所有数据应通过 invocation.getArgs() 或上下文传递。
-
不吞噬异常:拦截逻辑中捕获异常后, 需重新抛出,确保业务层能感知异常并处理;
-
避免循环依赖:多个插件间不可相互依赖,否则会导致调用链死循环。例如,A 插件调用 B,B 又反过来触发 A 的逻辑,极易导致栈溢出或死循环。设计时应保持插件职责单一、边界清晰。
三:MyBatis 插件 核心原理 :动态代理 + 责任链
核心工作机制:动态代理 + 责任链
MyBatis 插件的底层依赖两大核心机制,二者协同实现 “拦截 - 传递” 的逻辑:
-
JDK 动态代理:MyBatis 插件仅支持 JDK 动态代理(不支持 CGLIB),因为四大核心组件均为接口 ,动态代理需基于接口生成代理对象;
-
责任链模式:多个插件按顺序形成链式结构,每个插件处理后将调用传递给下一个插件,最终执行原始方法。
3.1 动态代理 + 责任链 是如何配合的?
传统链表式责任链,通过显式 next 指针串联处理器。
而 MyBatis 插件 不同于传统链表式责任链 ,MyBatis 采用了一种更为巧妙的方式 —— 代理嵌套 + 反射调用,形成一种“洋葱模型”的责任链 结构。
洋葱模型”的责任链 结构 : 每个插件并不直接持有下一个插件的引用,而是通过层层包裹代理对象的方式,将控制权逐层传递下去。
启动期:责任链 加载阶段:
启动期,责任链的 Interceptor 加载到 list。
一切始于 MyBatis 容器的初始化阶段。当 XMLConfigBuilder 解析<plugins> 配置标签时,便拉开了插件链构建的序幕。
此时,每一个 <plugin> 标签都会触发一次拦截器实例化流程。
MyBatis 不仅会通过无参构造函数创建 Interceptor 实例,还会自动调用其 setProperties() 方法注入配置参数,完成个性化设置。
所有实例化后的拦截器,最终都被统一添加到 Configuration 对象中的 interceptorChain 成员变量里 —— 一个 ArrayList<Interceptor>。
InterceptorChain 就加入了所有 的 Interceptor 到自己 的 interceptors 。
public class InterceptorChain {
// 核心:存储所有已注册的拦截器
private final List<Interceptor> interceptors = new ArrayList<>();
}
四大对象创建入口速查
| 对象 | 创建点 | 立即被 pluginAll |
|---|---|---|
| Executor | Configuration.newExecutor() | ✔ |
| StatementHandler | Configuration.newStatementHandler() | ✔ |
| ParameterHandler | Configuration.newParameterHandler() | ✔ |
| ResultSetHandler | Configuration.newResultSetHandler() | ✔ |
启动期, 责任链 装配 阶段:
基于 动态代理 ,实现 责任链 的 洋葱模式一层一层的 装配 。
当 MyBatis 开始创建四大核心组件(如 Executor)时,真正的魔法开始了。
在 Configuration.newXXX() 方法中,每创建一个核心对象,都会立即调用 interceptorChain.pluginAll(target) 方法,对该对象进行“全链路包装”。这个过程就像给一颗洋葱一层层地 裹上外皮。
public class Configuration {
// 1. Executor创建入口:接收事务对象,返回代理后的Executor
public Executor newExecutor(Transaction transaction) {
....
}
// 2. StatementHandler创建入口:接收Executor、MappedStatement等上下文
public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement,
....
}
// 3. ParameterHandler创建入口:接收SQL映射信息和参数
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
....
}
后续创建四大对象时,统一走 InterceptorChain.pluginAll(target):
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 一层洋葱皮
}
return target;
}
尼恩提示:这 上面的那一层包裹很重要。上面的代码使用了 默认 interceptor.plugin(target) 。
interceptor.plugin() 这个方法 是关键一环, 这里 返回 Plugin.wrap(target, this), 而 wrap 里边使用了 JDK 动态代理。
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;//核心拦截逻辑 - 必须由插件实现
/** * 默认的插件包装方法 - 关键默认实现! 90%的情况下使用这个默认实现即可 /
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}
启动期的 责任链+动态代理 结合 应用阶段, 代理包装 的过程如下:
proxy2 的 洋葱样式层层包裹 结构:proxy2(interceptor2) → proxy1(interceptor1) → 原始对象。
proxy2 的 洋葱样式 嵌套结构 示意:
// 实际的嵌套结构:
代理对象proxy2 = {
invocationHandler: Plugin对象2 {
target: 代理对象proxy1,
interceptor: 拦截器2
}
}
代理对象proxy1 = {
invocationHandler: Plugin对象1 {
target: 原始对象RealTarget,
interceptor: 拦截器1
}
}
Plugin.wrap() 源码 暗藏 JDK 动态代理。
public static Object wrap(Object target, Interceptor interceptor) {
.....
Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // ① 只代理“有签名”的接口
if (interfaces.length > 0) {
//jdk 动态代理
return Proxy.newProxyInstance( // ② 创建代理
type.getClassLoader(),
interfaces, //要实现的接口列表
new Plugin(target, interceptor, signatureMap)); ② 真代理
}
return target; // ③ 无匹配,原样返回
}
Plugin.wrap() 方法 ,属于 是创建代理对象的工厂。
① 处 :取得 出现的接口(如 StatementHandler) 。
② 处:JDK 动态代理要求 目标类必须实现接口,因此无法拦截四大接口以外的私有方法或final 类。
③ 处:若当前插件对 target 无兴趣,直接返回,减少代理层级。
运行时: 责任链执行阶段
责任链执行阶段 ,也就是 方法调用与链式执行 , 是运行时拦截 。
运行时拦截&执行 的过程:
-
触发目标方法:
业务代码调用 MyBatis 的 SqlSession 执行 SQL 时(如 sqlSession.selectList()),最终会调用四大核心组件的目标方法(如 Executor.query());
-
代理对象拦截:
由于核心组件已被代理,调用会先触发代理对象的 InvocationHandler.invoke() 方法(由 Plugin 类实现);
-
方法匹配校验:
Plugin.invoke() 方法会根据插件的 @Intercepts 和 @Signature 注解,校验当前调用的方法是否为需要拦截的方法(通过方法名、参数类型匹配);
-
执行插件逻辑:
若方法匹配,创建 Invocation 实例(封装目标对象、目标方法、参数),调用插件的 intercept(invocation) 方法,执行插件的自定义逻辑;
-
链式传递调用:
插件逻辑中通过 invocation.proceed() 触发下一级调用 —— 若存在多个插件,会调用下一个插件的代理逻辑;若为最后一个插件,则通过反射调用原始组件的目标方法;
-
结果返回:
原始方法执行完成后,返回结果按 “反向顺序” 经过每个插件的后置处理,最终返回给业务代码。
比如 当业务代码调用 sqlSession.selectList() 等方法时,底层最终会触发 Executor.query() 这样的核心方法。
但由于该对象早已被代理,实际执行的是代理对象的 invoke() 方法 —— 即 Plugin.invoke()。
Plugin.invoke() 伪代码:
public class Plugin implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 检查方法是否需要拦截
if (matches(method)) {
// 2. 创建 Invocation 对象,target 是下一层代理或原始对象
Invocation invocation = new Invocation(target, method, args);
// 3. 调用拦截器的 intercept 方法
return interceptor.intercept(invocation);
}
// 4. 不拦截,直接调用
return method.invoke(target, args);
}
}
Plugin.invoke() 调用 自定义 Plugin的intercept ,执行插件的逻辑。
@Intercepts(@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class}))
public class MyPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 前置处理
System.out.println("Before method execution");
// 关键:调用 proceed() 继续执行责任链
Object result = invocation.proceed();
// 后置处理
System.out.println("After method execution");
return result;
}
}
调用 Invocation.proceed() 继续执行责任链 :
/**
* 方法调用信息的封装类,也是责任链传递的关键载体
*/
public class Invocation {
// 三个核心字段都是 final,保证不可变性
private final Object target; // 目标对象(可能是原始对象或内层代理)
private final Method method; // 被调用的方法
private final Object[] args; // 方法参数
/**
* 构造函数
*/
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
/**
* 责任链传递的核心方法 - 关键!
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
// 核心:通过反射调用目标方法
return method.invoke(target, args);
}
}
method.invoke(target, args); 通过反射调用目标方法 , 目标对象 可能是原始对象或内层代理。
(1) 这个target是在创建Invocation对象时传入的,它可能是原始对象,也可能是内层的代理对象。
(2) 当target是代理对象时,调用其方法会触发下一个拦截器,从而形成责任链的传递。
(3) 责任链的传递是通过代理机制隐式实现的,而不是显式的链表结构。
因此 责任链 是通过“代理 包裹 代理” (洋葱模型责任链 ), 而非链表指针完成。
拦截顺序与洋葱模型责任链
假设注册 1 → 2 → 3,则代理嵌套: proxy3(proxy2(proxy1(original)))
调用顺序:
3.before → 2.before → 1.before → original → 1.after → 2.after → 3.after
先注册后执行,与 Servlet Filter 相同。
proxy3(proxy2(proxy1(original))) 调试时的调用栈深度分析
// 有3个拦截器时的调用栈:
executor.query() // 客户端调用
→ Proxy3.invoke() // 最外层代理
→ Interceptor3.intercept() // 拦截器3逻辑
→ invocation3.proceed() // 调用 proceed()
→ Proxy2.invoke() // 内层代理被触发
→ Interceptor2.intercept() // 拦截器2逻辑
→ invocation2.proceed() // 调用 proceed()
→ Proxy1.invoke() // 更内层代理
→ Interceptor1.intercept() // 拦截器1逻辑
→ invocation1.proceed() // 调用 proceed()
→ original.query() // 最终执行原始方法
不只是插件机制,更是一种架构思维
回顾整个流程,MyBatis 插件机制的本质,是一场 静态配置与动态代理的精密合谋:
- 在启动期,通过解析配置完成拦截器收集与代理链预组装;
- 在运行时,借助 JDK 动态代理与责任链模式,实现无侵入式的逻辑织入;
- 整个过程无需修改原有代码,却能达到类似 AOP 的效果。
更重要的是,这种“代理嵌套 + 反射传递”的责任链实现方式,展示了如何在有限的技术约束下(如仅支持接口代理),依然构建出灵活、可扩展的插件生态。
MyBatis 四大金刚接口的对象创建 与插件应用详解
MyBatis 四大金刚接口 的创建时机和插件应用机制。
四大金刚接口
| 对象 | 创建点 | 立即被 pluginAll | 说明 |
|---|---|---|---|
| Executor | Configuration.newExecutor() | ✔ | SQL执行器,第一个被创建和包装 |
| StatementHandler | Configuration.newStatementHandler() | ✔ | SQL语句处理器 |
| ParameterHandler | Configuration.newParameterHandler() | ✔ | 参数处理器 |
| ResultSetHandler | Configuration.newResultSetHandler() | ✔ | 结果集处理器 |
四大金刚接口 的 实例 创建流程
1. Executor 的 对象 创建过程
当一次数据库操作发起时,MyBatis 首先要做的就是创建一个 Executor 实例,它是整个 SQL 执行流程的调度中枢。
它的创建发生在 Configuration.newExecutor() 方法中,这个方法不仅决定了执行器的具体类型(如简单、批处理或可复用),还承担着两层关键职责:二级缓存封装、 插件链注入。
public class Configuration {
/**
* Executor 的创建入口
*/
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
// 1. 创建基础执行器
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);
}
// 2. 如果启用二级缓存,包装为CachingExecutor
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 3. 关键:应用所有插件(责任链包装)
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
}
这里有几个值得注意的技术细节:
- 策略模式的应用:根据配置选择不同的 Executor 实现,体现了典型的策略模式思想;
- 装饰器模式叠加:如果启用了二级缓存,会使用 CachingExecutor 对原始执行器进行包装,形成责任链结构;
- 插件链最后介入:无论是否有缓存,最终都要经过 interceptorChain.pluginAll() 处理,这意味着插件看到的是已经被缓存层包裹后的代理对象。
我们在开发插件时必须意识到:所拦截的对象 ,已经是多层代理后的复合体,不能假设其是“纯净”的原始实现。
2. StatementHandler的 对象 的创建过程
紧随 Executor 之后,MyBatis 开始准备 SQL 语句本身。
此时会调用 Configuration.newStatementHandler() 创建 StatementHandler。
这是真正负责将 Mapper 接口方法映射为 JDBC PreparedStatement 的核心组件。
public StatementHandler newStatementHandler(Executor executor,
MappedStatement mappedStatement,
Object parameterObject,
RowBounds rowBounds,
ResultHandler resultHandler,
BoundSql boundSql) {
// 1. 创建路由StatementHandler(根据语句类型选择具体实现)
StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement,
parameterObject, rowBounds,
resultHandler, boundSql);
// 2. 应用插件链
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
return statementHandler;
}
这里的 RoutingStatementHandler 是一个路由门面。
它会根据 SQL 类型(SELECT/INSERT/UPDATE/DELETE)动态委派给具体的子类(如 SimpleStatementHandler、PreparedStatementHandler 等)。
但无论具体实现如何变化,所有 StatementHandler 实例在返回前都会被插件链统一包装。
这使得 StatementHandler 成为了许多高级功能插件的首选目标,比如:
- SQL 改写类插件(分库分表、读写分离)
- SQL 安全审计(防 SQL 注入检测)
- 自动填充字段(创建时间、更新人等)
3. ParameterHandler 的 对象 的创建过程
参数处理器 ParameterHandler 负责将 Java 对象中的值设置到预编译语句的占位符中。
虽然它的职责看似单一,但它却是实现数据加密、脱敏、自动赋值等功能的理想位置。
public ParameterHandler newParameterHandler(MappedStatement mappedStatement,
Object parameterObject,
BoundSql boundSql) {
// 1. 创建默认参数处理器
ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement,
parameterObject, boundSql);
// 2. 应用插件链
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}
注意,这里创建的是 DefaultParameterHandler,它是唯一实现类,不可替换。
但由于它实现了接口且在创建后立即被插件链处理,因此我们仍然可以通过插件机制对其进行增强。
例如,在金融系统中常见的场景是:某些敏感字段(如身份证号、银行卡号)在入库前需加密存储。
传统做法是在业务层手动加密,容易遗漏;而借助插件机制,可以在 setParameters 被调用前自动识别标注了 @Encrypted 的字段并完成加密。
4. ResultSetHandler 的 对象 的创建过程
最后一个被创建的是 ResultSetHandler,它负责将 JDBC 的 ResultSet 映射为开发者期望的 POJO 或集合。
对于想要干预查询结果的场景(如字段脱敏、空值补全、性能监控),这是一个极其重要的拦截点。
public ResultSetHandler newResultSetHandler(Executor executor,
MappedStatement mappedStatement,
RowBounds rowBounds,
ParameterHandler parameterHandler,
ResultHandler resultHandler,
BoundSql boundSql) {
// 1. 创建默认结果集处理器
ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement,
parameterHandler, resultHandler,
rowBounds, boundSql);
// 2. 应用插件链
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
return resultSetHandler;
}
由于结果映射往往涉及复杂反射和嵌套处理,直接在业务代码中做后置处理效率低且难以复用。
而通过插件方式拦截 handleResultSets 方法,则可以在不改动任何 Mapper 接口的前提下,全局统一实现:
- 敏感字段脱敏(手机号中间四位变星号)
- 时间字段格式化(Date → LocalDateTime)
- 分页总数自动注入
2.5 可拦截方法完整列表
了解了四大金刚的对象创建流程后,下一步就是明确它们各自暴露了哪些可供插件拦截的方法。
这些方法构成了插件的能力边界。
这些方法 也是面试官常问的一个问题 “你知道 MyBatis 哪些方法可以被拦截吗?”的标准答案。
| 组件 | 可拦截方法 |
|---|---|
| Executor | update, query, queryCursor, flushStatements, commit, rollback, getTransaction, close, isClosed |
| StatementHandler | prepare, parameterize, batch, update, query, getBoundSql, getParameterHandler |
| ParameterHandler | setParameters, getParameterObject |
| ResultSetHandler | handleResultSets, handleCursorResultSets, handleOutputParameters |
这些方法的选择并非随机,而是围绕 SQL 生命周期的关键节点展开:
- Executor.query:适合做整体耗时统计、慢 SQL 记录;
- StatementHandler.prepare:适合修改 SQL 文本、添加额外条件;
- ParameterHandler.setParameters:适合参数加密、日志打印;
- ResultSetHandler.handleResultSets:适合结果脱敏、异常兜底。
MyBatis插件核心支撑组件源码解析
核心支撑 组件
除了四大核心组件(Executor、StatementHandler 等),插件机制的核心支撑组件包括:
| 组件名称 | 全类名 | 核心职责 |
|---|---|---|
| Interceptor | org.apache.ibatis.plugin.Interceptor | 插件核心接口,定义拦截逻辑(intercept)、代理生成(plugin)等方法 |
| InterceptorChain | org.apache.ibatis.plugin.InterceptorChain | 插件链管理器,存储所有注册的插件,负责生成代理链(pluginAll 方法) |
| Plugin | org.apache.ibatis.plugin.Plugin | 代理实现类,实现 InvocationHandler,负责匹配拦截方法并触发插件逻辑 |
| Invocation | org.apache.ibatis.plugin.Invocation | 调用信息封装类,封装目标对象、目标方法、方法参数,提供 proceed() 方法 |
| Intercepts | org.apache.ibatis.plugin.Intercepts | 注解,用于声明插件要拦截的组件和方法(包含多个 @Signature 注解) |
| Signature | org.apache.ibatis.plugin.Signature | 注解,精确指定拦截的组件类型(type)、方法名(method)、参数类型(args) |
【1】Interceptor:插件核心接口(定义扩展规范)
Interceptor是所有 MyBatis 插件的顶层接口,定义了插件必须实现的核心方法,是插件机制的 “扩展规范”。
Interceptor 接口是所有自定义插件必须实现的顶层契约,它定义了三个核心能力:拦截逻辑、代理生成、属性配置。
可以把它类比为 Spring AOP 中的 Advice,只不过它是基于 JDK 动态代理实现的轻量级方案。
package org.apache.ibatis.plugin;
import java.util.Properties;
/**
* 插件核心接口:所有自定义插件必须实现此接口
*/
public interface Interceptor {
/**
* 1. 核心拦截逻辑:插件的业务逻辑入口
* @param invocation 封装了目标对象、目标方法、方法参数的调用信息
* @return 目标方法的执行结果(可被插件修改后返回)
* @throws Throwable 允许抛出异常,由上层业务处理
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 2. 生成代理对象:决定是否为目标对象创建代理(默认委托Plugin.wrap()实现)
* @param target 被拦截的目标对象(如Executor、StatementHandler实例)
* @return 代理对象(若无需拦截则返回原始target)
*/
default Object plugin(Object target) {
// 委托Plugin工具类创建代理,避免每个插件重复实现代理逻辑
return Plugin.wrap(target, this);
}
/**
* 3. 配置属性:读取插件的配置参数(如XML中<property>标签配置的属性)
* @param properties 配置属性集合(key为属性名,value为属性值)
*/
default void setProperties(Properties properties) {
// 默认空实现:插件若需配置属性,可重写此方法
}
}
源码解读:
-
intercept:必须重写,是插件的核心业务逻辑所在(如 SQL 耗时统计、数据加密),通过invocation.proceed()触发后续调用; -
plugin:默认方法,通过Plugin.wrap()标准化代理生成逻辑,避免插件重复开发动态代理代码; -
setProperties:默认空实现,插件若需外部配置(如慢 SQL 阈值、加密密钥),可重写此方法读取属性。
尼恩提示:plugin() 和 setProperties() 都是默认方法,大多数情况下 只需要关注 intercept() 即可 。:
【2】InterceptorChain:插件链管理器(组织插件执行顺序)
如果说 Interceptor 是个体,那么 InterceptorChain 就是组织。
InterceptorChain是插件的 “容器与调度器”,负责存储所有注册的插件,并在核心组件创建时生成代理链,保证插件按注册顺序执行。
它是一个简单的 List 容器,但却承担着至关重要的职责:管理所有注册的插件,并在对象创建时依次调用其 plugin() 方法,形成层层嵌套的代理链。
package org.apache.ibatis.plugin;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 插件链管理器:管理所有注册的插件,生成代理链
*/
public class InterceptorChain {
// 存储所有注册的插件(线程安全:仅启动时添加,运行时只读)
private final List<Interceptor> interceptors = new ArrayList<>();
/**
* 核心方法:为目标对象生成代理链(插件依次包装目标对象)
* @param target 被拦截的核心组件(如Executor、StatementHandler)
* @return 最终的代理对象(多层代理:插件1→插件2→原始target)
*/
public Object pluginAll(Object target) {
// 遍历所有插件,依次为目标对象创建代理(注册顺序=执行顺序)
for (Interceptor interceptor : interceptors) {
// 调用每个插件的plugin()方法,生成新的代理对象
target = interceptor.plugin(target);
}
return target;
}
/**
* 添加插件到链中(MyBatis启动时解析配置后调用)
* @param interceptor 插件实例
*/
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/**
* 获取所有插件(只读,避免外部修改插件列表)
* @return 不可修改的插件列表
*/
public List<Interceptor> getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
源码解读:
-
pluginAll:核心逻辑,通过循环调用每个插件的plugin()方法,将目标对象层层包装为代理(如注册 2 个插件:target → 插件2代理 → 插件1代理); -
addInterceptor:MyBatis 启动时,从配置中解析插件后调用此方法添加到列表; -
getInterceptors:返回不可修改的列表,防止运行时插件列表被意外篡改,保证线程安全。
这个 pluginAll() 方法看似简单,实则暗藏玄机:
- 执行顺序 = 注册顺序:先注册的插件会被后注册的插件“包在外面”,也就是说,调用时会最先触发;
- 链式包装:每次 plugin() 返回的都是一个新的代理对象,作为下一个插件的输入目标;
- 只读保障:插件列表一旦初始化完成就不再修改,确保运行时线程安全。
【3】Plugin:代理实现类(动态代理的 “执行者”)
Plugin是 MyBatis 动态代理的核心实现类,实现InvocationHandler接口,负责:
1、校验方法是否需要拦截;
2、触发插件的intercept逻辑;
3、调用原始方法或下一个代理。
Plugin 类 实现了 InvocationHandler 接口,是 JDK 动态代理的实际处理器。
Plugin 类是整个插件机制中最核心的技术实现, 可以把它看作是 MyBatis 自研的“AOP 代理生成器”。
package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 动态代理实现类:负责拦截方法调用,触发插件逻辑
*/
public class Plugin implements InvocationHandler {
// 原始目标对象(如Executor实例,非代理对象)
private final Object target;
// 对应的插件实例(当前代理关联的Interceptor)
private final Interceptor interceptor;
// 插件要拦截的方法映射(key:组件接口类型,value:该接口下需拦截的方法集合)
private final Map<Class<?>, Set<Method>> signatureMap;
/**
* 私有构造:仅通过wrap()方法创建实例(避免外部随意创建)
*/
private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {
this.target = target;
this.interceptor = interceptor;
this.signatureMap = signatureMap;
}
/**
* 静态工厂方法:创建代理对象(插件生成代理的统一入口)
* @param target 目标对象
* @param interceptor 插件实例
* @return 代理对象(若无需拦截则返回原始target)
*/
public static Object wrap(Object target, Interceptor interceptor) {
// 1. 解析插件的@Intercepts和@Signature注解,获取需拦截的方法映射
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
// 2. 获取目标对象的所有接口(JDK动态代理需基于接口)
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
// 3. 若存在需拦截的接口,生成代理;否则返回原始对象
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(), // 目标对象的类加载器
interfaces, // 目标对象实现的接口(需拦截的接口)
new Plugin(target, interceptor, signatureMap) // 代理处理器
);
}
return target;
}
/**
* 代理核心逻辑:方法调用时触发(JDK动态代理的核心方法)
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// 1. 获取当前方法所属接口需拦截的方法集合
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
// 2. 若当前方法需拦截,触发插件的intercept逻辑
if (methods != null && methods.contains(method)) {
// 封装调用信息为Invocation,传递给插件
return interceptor.intercept(new Invocation(target, method, args));
}
// 3. 若无需拦截,直接调用原始目标对象的方法
return method.invoke(target, args);
} catch (Exception e) {
// unwrapThrowable:解包异常(如InvocationTargetException),返回真实异常
throw ExceptionUtil.unwrapThrowable(e);
}
}
/**
* 解析插件的@Intercepts注解,生成“接口→方法集合”的映射
*/
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
// 获取插件类上的@Intercepts注解
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
// 若未加@Intercepts注解,抛出异常(插件必须声明拦截规则)
if (interceptsAnnotation == null) {
throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());
}
// 获取@Intercepts注解中的@Signature数组(每个@Signature对应一个拦截规则)
Signature[] sigs = interceptsAnnotation.value();
Map<Class<?>, Set<Method>> signatureMap = new HashMap<>();
for (Signature sig : sigs) {
// 获取@Signature中指定的组件接口(如Executor.class)
Set<Method> methods = signatureMap.computeIfAbsent(sig.type(), k -> new HashSet<>());
try {
// 根据方法名和参数类型,获取接口中的目标方法
Method method = sig.type().getMethod(sig.method(), sig.args());
methods.add(method);
} catch (NoSuchMethodException e) {
throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
}
}
return signatureMap;
}
/**
* 筛选目标对象实现的接口中,需要被拦截的接口
*/
private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {
Set<Class<?>> interfaces = new HashSet<>();
// 遍历目标对象的所有接口(包括父类实现的接口)
while (type != null) {
for (Class<?> c : type.getInterfaces()) {
// 若接口在signatureMap中(即插件需拦截此接口),加入结果集
if (signatureMap.containsKey(c)) {
interfaces.add(c);
}
}
type = type.getSuperclass();
}
// 转为数组返回(JDK动态代理需传入接口数组)
return interfaces.toArray(new Class<?>[0]);
}
}
源码解读:
(1) wrap:静态工厂方法,是代理生成的入口,分三步:解析注解→筛选接口→生成代理;
(2) invoke:代理的核心触发逻辑,先校验方法是否在拦截列表中,是则执行插件intercept,否则调用原始方法;
(3) getSignatureMap:解析@Intercepts和@Signature注解,将拦截规则转为 “接口→方法” 映射,避免每次调用重复解析;
(4) getAllInterfaces:筛选目标对象中需拦截的接口,确保代理仅针对插件关注的接口生成,减少不必要的代理开销。
这段代码体现了三个精巧设计:
(1) 注解驱动配置:通过 @Intercepts + @Signature 声明式指定拦截点,替代繁琐的 XML 配置;
(2) 精确匹配机制:args 参数用于区分重载方法,避免误拦截;
(3) 最小代理原则:只对接口中有声明拦截的方法生成代理,减少性能损耗。
【4】Invocation:调用信息封装类(传递调用上下文)
Invocation是插件调用的 “上下文载体”,封装了目标对象、目标方法、方法参数,提供proceed()方法触发后续调用,解耦插件与原始方法的直接依赖。
Invocation 是插件开发中最常用的辅助类,它封装了目标对象、方法和参数,提供了一个干净的调用上下文。
package org.apache.ibatis.plugin;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* 调用信息封装类:封装目标方法的调用上下文
*/
public class Invocation {
// 原始目标对象(可能是下一个插件的代理,或最终的核心组件实例)
private final Object target;
// 被调用的目标方法
private final Method method;
// 目标方法的参数数组
private final Object[] args;
/**
* 构造方法:封装调用信息
*/
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
// Getter方法:供插件获取调用信息(如修改参数、获取方法名)
public Object getTarget() {
return target;
}
public Method getMethod() {
return method;
}
public Object[] getArgs() {
return args;
}
/**
* 核心方法:触发后续调用(下一个插件或原始方法)
* @return 目标方法的执行结果
* @throws InvocationTargetException 方法调用异常
* @throws IllegalAccessException 权限异常
*/
public Object proceed() throws InvocationTargetException, IllegalAccessException {
// 反射调用目标对象的方法(若target是代理,则触发下一个插件的invoke逻辑)
return method.invoke(target, args);
}
}
源码解读:
-
封装性:将调用相关的三个核心要素(target、method、args)封装,插件无需直接处理反射细节;
-
proceed():是调用链传递的关键,插件执行前置逻辑后调用此方法,触发下一个插件或原始方法,实现 “链式执行”; -
可扩展性:插件可通过 Getter 方法修改参数(如
getArgs()[0] = newParam)或获取方法信息(如getMethod().getName()),实现灵活的逻辑干预。
它的最大价值体现在 proceed() 方法上:它是责任链向下传递的开关。插件可以在调用 proceed() 前后插入逻辑,实现环绕通知的效果。
举个例子,统计 SQL 执行时间的经典写法:
long start = System.currentTimeMillis();
Object result = invocation.proceed(); // 触发后续调用
long cost = System.currentTimeMillis() - start;
log.info("SQL executed in {} ms", cost);
return result;
此外,你还可通过 getArgs() 修改参数、通过 getMethod() 获取方法名做路由分发,灵活性极高。
【5】Intercepts 与 Signature:注解(声明拦截规则)
Intercepts和Signature是 “注解式配置”,用于声明插件的拦截规则(拦截哪个组件、哪个方法),替代 XML 配置,简化插件开发。
二者均为元注解,仅用于标记和传递配置信息。
5.1 Intercepts 注解(插件的拦截规则容器)
package org.apache.ibatis.plugin;
import java.lang.annotation.*;
/**
* 插件拦截规则容器:标注在插件类上,包含多个@Signature注解
*/
@Documented
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,供Plugin类解析
@Target(ElementType.TYPE) // 仅可标注在类上(插件类)
public @interface Intercepts {
// 多个@Signature注解(一个插件可拦截多个方法)
Signature[] value();
}
5.2 Signature 注解(单个拦截规则)
package org.apache.ibatis.plugin;
import java.lang.annotation.*;
/**
* 单个拦截规则:指定拦截的组件、方法、参数
*/
@Documented
@Retention(RetentionPolicy.RUNTIME) // 运行时保留,供Plugin类解析
@Target({}) // 无目标类型:仅可在@Intercepts中使用
public @interface Signature {
// 拦截的组件接口类型(如Executor.class、StatementHandler.class)
Class<?> type();
// 拦截的方法名(如"query"、"update"、"setParameters")
String method();
// 拦截方法的参数类型数组(用于精确匹配方法,避免方法重载冲突)
Class<?>[] args();
}
源码解读:
-
元数据作用:二者均为运行时保留的注解,仅用于传递拦截规则,无业务逻辑,核心解析逻辑在
Plugin.getSignatureMap()中; -
精确匹配:
args参数是关键,可解决方法重载问题(如Executor.query有多个重载方法,通过参数类型区分); -
灵活性:一个
@Intercepts可包含多个@Signature,支持一个插件拦截多个组件或多个方法(如同时拦截Executor.query和Executor.update)。
mybatis 的3大不足
MyBatis 在实际项目中仍存在一些反模式和潜在风险,稍有不慎就会引发线上故障。
这些往往是面试官压轴提问的方向:“你觉得 MyBatis 插件有什么缺点?”
1. 金字塔陷阱:层层修改 SQL,最终面目全非
当多个插件依次修改 SQL 时(如分页插件改 LIMIT,租户插件加 tenant_id,安全插件过滤敏感词),最终生成的 SQL 与原始 XML 完全不符,调试时极难追踪。
规避方案:统一 SQL 改写入口,或记录每一步的变更日志。
2. 循环增强:插件内部调用 Mapper 导致递归爆栈
常见错误是在插件的 intercept() 中再次调用 sqlSession.selectList(),结果触发新一轮拦截,形成无限递归,最终 StackOverflowError。
规避方案:
- 插件内部禁止调用 Mapper;
- 使用 ThreadLocal 标记当前是否已在拦截流程中;
- 或通过 Ordered 接口显式控制执行顺序。
3. 热部署失效:DevTools 重启后插件重复注册
Spring Boot DevTools 重启时,SqlSessionFactory 是新实例,但如果插件被注册到了 static 列表中,旧实例未清理,就会导致同一插件被多次加载,出现重复拦截。
规避方案:避免使用 static 集合存储插件;推荐通过 Spring 容器管理插件生命周期。
mybatis使用的雷区
MyBatis 保证 一个 SqlSessionFactory 对应一套 Interceptor 单例,因此
- 禁止在插件里存 请求级 变量 → 用 ThreadLocal 或 Invocation 临时传参。
- 禁止开线程池 → 生命周期与 MyBatis 不一致易内存泄漏。
MyBatis 核心架构思想
MyBatis 插件机制的 “动态代理 + 责任链” 组合,并非单纯的技术堆砌,而是对开闭原则、单一职责、最小侵入性等经典架构原则的深度落地,形成了 “灵活扩展与核心稳定共存” 的设计哲学。
一:OCP 开闭原则:扩展开放,修改关闭
思想内涵:
允许在不修改框架核心源码的前提下,通过扩展机制增强功能,是插件机制的核心设计准则。
落地细节:
-
核心组件(Executor、StatementHandler 等)以接口形式定义,避免因实现类修改影响全局;
-
插件通过Interceptor接口接入,无需修改 MyBatis 核心流程(如 SQL 执行链路);
示例:
新增 “SQL 加密” 功能时,只需实现Interceptor接口定义插件,无需改动Executor或StatementHandler的原始实现,符合 “不修改核心代码” 的开闭原则。
从源码看 OCP 如何落地
Configuration 创建对象时统一:
executor = (Executor) interceptorChain.pluginAll(executor);
此后无论新增多少插件,Configuration 源码永不改动,符合 Open-Close。
如何理解 这里的 “开放”与“封闭”?
-
开放:需求变化时,允许通过新增代码(继承、实现新类、插件等)来扩展功能。
-
封闭:扩展时尽量不改动已有源码或二进制,避免引入回归缺陷
在 MyBatis 中,四大核心对象(Executor、StatementHandler…)的创建逻辑被固化在 Configuration 里,但框架通过 InterceptorChain.pluginAll() 给它们套上一层 JDK 动态代理:
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target); // 层层包裹,不改动原类
}
return target;
}
当需要增加“分页 / 加密 / 多租户”等横切功能时:
-
只新增一个实现
Interceptor的插件类; -
无需修改
Configuration、DefaultSqlSessionFactory等任何核心源码;
插件通过注解声明签名,框架在运行时动态代理织入,既扩展了能力,又保证了旧代码零触碰——这就是 OCP 的经典实践
OCP 要求“不改旧代码,只写新代码”;
MyBatis 用“动态代理 + 责任链” 真正做到:“想加功能?写个插件就行, core 文件一眼不看。”
二: SRP 单一职责原则:职责分离,各司其职
… 略5000字+
…由于平台篇幅限制, 剩下的内容(5000字+),请参参见原文地址

被折叠的 条评论
为什么被折叠?



