😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 🤩 🥰 😘 😗 😙 😋 😛 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 😠 😤 😭
MyBatis 核心知识点详解
一、配置加载与初始化:从 XML 到 Configuration 对象
MyBatis 的初始化流程始于 SqlSessionFactoryBuilder
对配置文件的解析,最终生成全局唯一的 SqlSessionFactory
。这一过程涉及复杂的 XML 解析、属性注入和对象创建。
1.1 配置文件解析流程
- 入口类 :
SqlSessionFactoryBuilder
的build(InputStream inputStream)
方法。 - 解析步骤 :
- 读取
mybatis-config.xml
:使用XMLConfigBuilder
解析根节点<configuration>
。 - 解析子节点 :
<properties>
:加载外部属性文件(如db.properties
),支持${}
占位符替换。<settings>
:配置 MyBatis 核心参数(如cacheEnabled
、lazyLoadingEnabled
、defaultExecutorType
),通过Settings
对象存储。<typeAliases>
:定义类型别名(如typeAlias="User"
),简化parameterType
和resultType
的书写。<environments>
:配置数据库环境(development
/production
),包含dataSource
(数据源)和transactionManager
(事务管理器)。<mappers>
:注册 Mapper 接口或 XML 映射文件(支持resource
、package
、class
三种方式)。
- 构建
Configuration
对象 :将所有解析结果封装到Configuration
实例中,作为 MyBatis 的"大脑"。
- 读取
1.2 关键配置项解析
-
dataSource
:默认使用UNPOOLED
(非连接池),生产环境通常替换为POOLED
(连接池)或第三方实现(如 HikariCP)。xml<dataSource type="POOLED"> <property name="driver" value="${driver}"/> <property name="url" value="${url}"/> <property name="username" value="${username}"/> <property name="password" value="${password}"/> </dataSource>
-
transactionManager
:默认JDBC
(基于 JDBC 事务),可选MANAGED
(由容器管理事务)。 -
mappers
:注册 Mapper 的三种方式:xml<!-- 方式1:直接指定 XML 映射文件 --> <mapper resource="com/example/mapper/UserMapper.xml"/> <!-- 方式2:扫描包(接口与 XML 同名同目录) --> <package name="com.example.mapper"/> <!-- 方式3:注解绑定(接口与 XML 分离) --> <mapper class="com.example.mapper.UserMapper"/>
二、SqlSession 的创建与管理:会话生命周期
SqlSession
是 MyBatis 的核心接口,负责数据库操作和事务管理。其创建过程涉及 Executor
的初始化和 Configuration
的注入。
2.1 SqlSession 的创建流程
- 入口方法 :
SqlSessionFactory.openSession()
。 - 核心逻辑 :
- 确定事务隔离级别 :默认
Connection.TRANSACTION_READ_COMMITTED
(可读已提交)。 - 创建
Executor
:根据Configuration.defaultExecutorType
(默认SIMPLE
)选择执行器类型。 - 生成
DefaultSqlSession
:持有Executor
、Configuration
和事务状态(autoCommit
)。
- 确定事务隔离级别 :默认
2.2 SqlSession 的核心方法
方法 | 说明 |
---|---|
getMapper(Class<T> type) |
获取 Mapper 接口的代理对象(JDK 动态代理实现)。 |
selectOne(String statement) |
执行查询,返回单个结果(内部调用 selectList 并取第一个元素)。 |
insert/update/delete |
执行增删改操作,返回影响的行数。 |
commit() /rollback() |
提交/回滚事务(仅当 autoCommit=false 时有效)。 |
close() |
关闭会话,释放连接回连接池。 |
2.3 SqlSession 的线程安全性
-
非线程安全 :
SqlSession
应仅在单个线程中使用(类似Connection
)。- 最佳实践 :通过
try-with-resources
确保及时关闭:
javatry (SqlSession session = sqlSessionFactory.openSession()) { UserMapper mapper = session.getMapper(UserMapper.class); User user = mapper.selectById(1L); }
- 最佳实践 :通过
三、Executor 执行器:SQL 实际执行者
Executor
是 MyBatis 的 SQL 执行核心,负责管理 PreparedStatement
的生命周期、批处理和事务。
3.1 三种 Executor 的实现
-
SimpleExecutor
(默认):-
特点 :每次执行 SQL 时创建新的
PreparedStatement
,执行后关闭。 -
适用场景 :简单查询,无需重复使用
PreparedStatement
。 -
执行流程:
java// 伪代码:SimpleExecutor 执行 select public <E> List<E> query(MappedStatement ms, Object parameter) { Connection conn = getConnection(); // 从连接池获取连接 PreparedStatement stmt = conn.prepareStatement(ms.getBoundSql(parameter).getSql()); setParameters(stmt, parameter); // 设置参数(#{name} → ?) ResultSet rs = stmt.executeQuery(); return rsToResult(rs, ms); // 结果映射 }
-
-
ReuseExecutor
:- 特点 :缓存
PreparedStatement
(按 SQL 字符串哈希),重复使用以减少 JDBC 调用开销。 - 适用场景:高频执行相同 SQL(如批量插入中的单条语句)。
- 缓存结构 :
Map<String, PreparedStatement> statementMap
。
- 特点 :缓存
-
BatchExecutor
:-
特点 :通过
addBatch()
累积 SQL 参数,最后通过executeBatch()
批量执行。 -
适用场景:大数据量插入/更新(如百万级数据导入)。
-
执行流程:
java// 伪代码:BatchExecutor 执行 batch insert public int[] batchInsert(List<User> users) { Connection conn = getConnection(); PreparedStatement stmt = conn.prepareStatement("INSERT INTO user (name) VALUES (?)"); for (User user : users) { stmt.setString(1, user.getName()); stmt.addBatch(); // 累积参数 } return stmt.executeBatch(); // 批量执行 }
-
3.2 Executor 与事务的关系
- 事务控制 :
Executor
通过Connection
的setAutoCommit(false)
开启事务,commit()
/rollback()
提交/回滚。 - 连接管理 :
Executor
从DataSource
获取连接(支持连接池),并在SqlSession
关闭时释放。
四、MappedStatement:SQL 的元数据封装
MappedStatement
是 MyBatis 中 SQL 语句的元数据对象,封装了 SQL 模板、参数类型、结果类型等关键信息。
4.1 MappedStatement 的生成流程
- 解析映射文件 :
XMLMapperBuilder
解析<select>
,<insert>
,<update>
,<delete>
标签。 - 构建
BoundSql
:将 SQL 模板(含动态标签)转换为BoundSql
对象,解析参数占位符(#{}
)和结果映射。 - 注册到 Configuration :将
MappedStatement
存入Configuration.mappedStatements
(键为namespace.id
)。
4.2 MappedStatement 的核心属性
属性 | 说明 |
---|---|
id |
SQL 语句的唯一标识(对应 Mapper 接口方法名)。 |
parameterType |
参数类型(Java 类或别名,如 User 或 java.lang.String )。 |
resultType |
结果类型(Java 类或别名,如 User )。 |
resultMap |
自定义结果映射(解决字段名与属性名不一致问题)。 |
sqlSource |
SQL 源(StaticSqlSource 或 DynamicSqlSource ,动态 SQL 用后者)。 |
keyGenerator |
主键生成策略(Jdbc3KeyGenerator 或 SelectKeyGenerator )。 |
4.3 动态 SQL 的解析:DynamicSqlSource
-
动态标签处理 :
<if>
,<foreach>
,<where>
等标签通过SqlNode
接口实现,最终生成完整的 SQL。 -
OGNL 表达式解析 :使用 OGNL 引擎解析
<if test="name != null">
等条件表达式。 -
示例:
xml<select id="selectUser" resultType="User"> SELECT * FROM user <where> <if test="id != null">id = #{id}</if> <if test="name != null">AND name LIKE CONCAT('%', #{name}, '%')</if> </where> </select>
- 解析后生成的
BoundSql.sql
可能为:SELECT * FROM user WHERE id = ? AND name LIKE CONCAT('%', ?, '%')
。
- 解析后生成的
五、参数映射(#{}):从 Java 到 JDBC 的转换
#{}
是 MyBatis 的参数占位符,负责将 Java 对象转换为 JDBC 参数(PreparedStatement.setXXX()
)。
5.1 ParameterHandler 的执行流程
- 获取参数对象 :从
MappedStatement
中获取参数类型(parameterType
)。 - 创建
TypeHandler
:根据参数类型选择对应的TypeHandler
(如StringTypeHandler
、IntegerTypeHandler
)。 - 设置参数值 :调用
TypeHandler.setParameter(PreparedStatement, index, value, jdbcType)
方法。
5.2 复杂参数的处理
- POJO 对象 :通过
OGNL
表达式提取属性值(如#{user.name}
提取user
对象的name
属性)。 - 集合类型 :支持
List
、Array
等,通过#{list[0]}
访问元素(需配合<foreach>
标签)。 - 命名参数 :通过
@Param
注解指定参数名(如@Param("userName") String name
),#{userName}
直接引用。
5.3 TypeHandler 的自定义
-
实现
TypeHandler
接口:javapublic class LocalDateTimeTypeHandler implements TypeHandler<LocalDateTime> { @Override public void setParameter(PreparedStatement ps, int i, LocalDateTime parameter, JdbcType jdbcType) { ps.setTimestamp(i, Timestamp.valueOf(parameter)); // LocalDateTime → Timestamp } }
-
注册到 MyBatis :在
mybatis-config.xml
中配置:xml<typeHandlers> <typeHandler handler="com.example.handler.LocalDateTimeTypeHandler"/> </typeHandlers>
六、结果映射(ResultMap):从 ResultSet 到 Java 对象
ResultMap
是 MyBatis 的结果集映射核心,负责将 ResultSet
的列与 Java 对象的属性匹配。
6.1 ResultSetHandler 的执行流程
- 获取
ResultSet
:通过Statement.executeQuery()
获取结果集。 - 创建
ResultLoader
:处理嵌套查询(association
、collection
)。 - 映射结果 :根据
ResultMap
将列值赋值给对象属性(支持自动映射和自定义映射)。
6.2 嵌套查询的实现
-
association
(一对一) :通过select
属性指定关联查询的 Mapper 方法,column
传递外键。xml<resultMap id="userMap" type="User"> <id column="user_id" property="id"/> <association property="order" column="user_id" select="selectOrderById"/> </resultMap>
-
collection
(一对多) :类似association
,但处理集合类型。xml<resultMap id="orderMap" type="Order"> <id column="order_id" property="id"/> <collection property="items" column="order_id" select="selectItemsByOrderId"/> </resultMap>
6.3 延迟加载的触发
-
代理对象生成 :MyBatis 使用 CGLIB 为结果对象生成代理类,重写关联字段的
getter
方法。 -
拦截器逻辑 :当调用代理对象的
getOrder()
方法时,触发拦截器,执行关联查询并填充数据。 -
配置延迟加载:
xml<!-- 全局启用延迟加载 --> <settings> <setting name="lazyLoadingEnabled" value="true"/> <setting name="aggressiveLazyLoading" value="false"/> <!-- 关闭激进加载 --> </settings>
七、插件机制:拦截器的链式调用
MyBatis 的插件机制通过动态代理(JDK/CGLIB)拦截四大核心接口(Executor
、StatementHandler
、ParameterHandler
、ResultSetHandler
)的方法,实现功能扩展(如分页、日志)。
7.1 插件的注册与执行
- 定义插件类 :实现
Interceptor
接口,重写intercept(Invocation)
方法。
java
public class PagePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 拦截 Executor.query() 方法
if (invocation.getTarget() instanceof Executor) {
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
BoundSql boundSql = ms.getBoundSql(invocation.getArgs()[1]);
// 修改 SQL 添加 LIMIT
String newSql = boundSql.getSql() + " LIMIT 10";
// 替换 MappedStatement 的 SQL
MappedStatement newMs = copyFromNewSql(ms, newSql);
invocation.getArgs()[0] = newMs; // 替换目标对象
}
return invocation.proceed(); // 继续执行原方法
}
}
- 注册插件 :在
mybatis-config.xml
中配置:
xml
<plugins>
<plugin interceptor="com.example.PagePlugin"/>
</plugins>
7.2 拦截器的链式调用
InterceptorChain
:管理所有插件的拦截顺序,通过pluginAll()
方法为目标对象生成代理。- 代理类型 :
- JDK 动态代理 :目标对象实现了接口(如
Executor
接口)。 - CGLIB 代理 :目标对象未实现接口(如
StatementHandler
的实现类)。
- JDK 动态代理 :目标对象实现了接口(如
7.3 分页插件原理(以 PageHelper 为例)
- 拦截
Executor.query()
:解析分页参数(pageNum
、pageSize
)。 - 修改 SQL :将原始 SQL 包裹为
SELECT * FROM (原 SQL) LIMIT offset, pageSize
(MySQL)或ROWNUM
(Oracle)。 - 缓存总记录数 :通过
COUNT(*)
查询获取总页数,填充到Page
对象。
八、事务管理:从 JDBC 到 Spring 集成
MyBatis 的事务管理基于 JDBC 事务,支持手动提交和声明式事务(与 Spring 集成时)。
8.1 事务的生命周期
- 开启事务 :
SqlSession
初始化时,通过Connection.setAutoCommit(false)
开启事务。 - 执行操作 :执行增删改查,结果暂存于
Connection
。 - 提交/回滚 :
session.commit()
调用Connection.commit()
;session.rollback()
调用Connection.rollback()
。 - 关闭连接 :
session.close()
释放连接回连接池。
8.2 与 Spring 的集成
SqlSessionTemplate
:Spring 提供的SqlSession
包装类,解决线程安全问题(通过ThreadLocal
绑定当前线程的SqlSession
)。MapperFactoryBean
:将 Mapper 接口注册为 Spring Bean,通过proxyFactory
生成代理对象。- 声明式事务 :通过
@Transactional
注解,由PlatformTransactionManager
管理SqlSession
的提交/回滚。
8.3 事务隔离级别
- 支持级别 :
READ_UNCOMMITTED
、READ_COMMITTED
、REPEATABLE_READ
、SERIALIZABLE
(通过transactionManager
配置)。 - 默认级别 :
READ_COMMITTED
(与大多数数据库默认一致)。
九、缓存机制:提升查询性能
MyBatis 提供一级缓存(SqlSession
级别)和二级缓存(Mapper
级别),减少数据库访问次数。
9.1 一级缓存
- 作用域 :与
SqlSession
绑定,SqlSession
关闭后失效。 - 存储结构 :
PerpetualCache
(基于HashMap
)。 - 失效场景 :
- 执行
update
、insert
、delete
操作(默认清空缓存)。 - 手动调用
session.clearCache()
。
- 执行
- 配置 :默认开启,无法关闭(可通过
localCacheScope=STATEMENT
禁用)。
9.2 二级缓存
-
作用域 :与
Mapper
(namespace
)绑定,多个SqlSession
共享。 -
启用条件:
- 全局配置
mybatis-config.xml
中cacheEnabled=true
(默认false
)。 - 映射文件中添加
<cache/>
标签(可选配置eviction="LRU"
、flushInterval="60000"
)。
- 全局配置
-
序列化要求 :缓存对象需实现
Serializable
接口(否则无法持久化到磁盘)。 -
示例:
xml<mapper namespace="com.example.mapper.UserMapper"> <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/> </mapper>
9.3 缓存的失效与更新
- 自动失效 :当
Mapper
中的 SQL 执行了写操作(update
、insert
、delete
),二级缓存会被清空。 - 手动刷新 :调用
SqlSession.clearCache()
(一级)或Mapper.clearCache()
(二级)。
十、动态 SQL 的底层解析:OGNL 与 SqlNode
动态 SQL 是 MyBatis 的核心特性之一,通过 OGNL 表达式和 SqlNode
接口实现灵活的 SQL 拼接。
10.1 OGNL 表达式解析
- OGNL 引擎 :用于计算
<if test="...">
中的表达式(如name != null
)。 - 上下文对象 :
OgnlContext
包含参数对象(root
)、@Param
注解的参数(param1
,param2
)等。
10.2 SqlNode 的实现类
SqlNode 类型 | 作用 |
---|---|
StaticTextSqlNode |
静态文本(如 SELECT * FROM user )。 |
IfSqlNode |
条件判断(<if test="..."> )。 |
ForEachSqlNode |
循环遍历集合(<foreach collection="list" item="item"> )。 |
WhereSqlNode |
自动添加 WHERE 关键字并去除多余的 AND/OR (<where> )。 |
SetSqlNode |
自动添加 SET 关键字并去除多余的 , (<set> )。 |
TrimSqlNode |
自定义前缀和后缀(如去除多余的 , 或 AND )。 |
10.3 动态 SQL 的执行流程
- 解析 XML 映射文件 :
XMLMapperBuilder
将<select>
标签转换为MappedStatement
。 - 生成
BoundSql
:DynamicSqlSource
解析SqlNode
树,生成最终的 SQL 字符串和参数映射。 - 执行 SQL :
Executor
使用BoundSql
中的 SQL 和参数执行数据库操作。
十一、延迟加载的底层实现:CGLIB 与 MetaObject
延迟加载通过 CGLIB 生成代理对象,在访问关联字段时触发查询,避免一次性加载所有数据。
11.1 CGLIB 代理的生成
-
Enhancer
类:CGLIB 的核心工具,用于生成目标类的子类代理。 -
MethodInterceptor
:代理对象的方法拦截器,重写intercept
方法。 -
示例:
java// CGLIB 生成代理对象的伪代码 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(User.class); enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) { if ("getOrder".equals(method.getName())) { // 触发关联查询 Long userId = ((User) obj).getId(); Order order = orderMapper.selectByUserId(userId); ((User) obj).setOrder(order); // 填充关联对象 return order; } return proxy.invokeSuper(obj, args); } }); User proxyUser = (User) enhancer.create(); // 生成代理对象
11.2 MetaObject 的作用
- 属性访问 :通过
MetaObject
动态获取/设置对象的属性值(支持嵌套对象)。 - 关联查询触发 :在代理对象的
getter
方法中,通过MetaObject
获取外键值,执行关联查询。
11.3 延迟加载的性能优化
- 批量加载 :通过
@BatchSize
注解指定批量加载大小(如@BatchSize(size=10)
),减少数据库查询次数。 - 全局配置 :
lazyLoadingEnabled=true
启用延迟加载,aggressiveLazyLoading=false
关闭激进加载(默认)。
十二、与 Spring 集成的深度整合
MyBatis 与 Spring 集成后,通过 SqlSessionTemplate
和 MapperScannerConfigurer
简化开发,实现自动事务管理。
12.1 SqlSessionTemplate 的作用
- 线程安全 :通过
ThreadLocal
绑定当前线程的SqlSession
,解决SqlSession
非线程安全问题。 - 自动提交 :与 Spring 事务同步,事务开启时
autoCommit=false
,事务提交时调用commit()
。 - 异常处理 :将 JDBC 异常转换为 Spring 的
DataAccessException
。
12.2 Mapper 扫描器(MapperScannerConfigurer)
-
自动注册 Mapper:扫描指定包下的 Mapper 接口,生成代理对象并注册为 Spring Bean。
-
配置示例:
xml<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mapper"/> <!-- Mapper 接口包路径 --> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!-- SqlSessionFactory 的 Bean 名称 --> </bean>
12.3 声明式事务管理(@Transactional)
- 事务拦截器 :Spring 的
TransactionInterceptor
拦截标注@Transactional
的方法,通过PlatformTransactionManager
管理事务。 - 与 MyBatis 集成 :
PlatformTransactionManager
使用SqlSessionTemplate
的getConnection()
获取数据库连接,实现事务同步。
十三、性能优化与最佳实践
13.1 减少数据库交互
- 批量操作 :使用
BATCH
执行器或<foreach>
标签减少INSERT
/UPDATE
次数。 - 缓存优化:高频读、低频写场景启用二级缓存;写频繁场景关闭缓存。
13.2 SQL 优化
- 索引优化 :
WHERE
、JOIN
、ORDER BY
字段添加索引。 - 避免
SELECT *
:明确指定需要的字段,减少数据传输量。 - 分页查询 :使用
LIMIT
或ROWNUM
限制返回行数,避免全表扫描。
13.3 连接池配置
- 选择高性能连接池:如 HikariCP(默认)、Druid(支持监控)。
- 参数调优 :设置合理的
maxActive
(最大连接数)、maxIdle
(最大空闲连接数)、minIdle
(最小空闲连接数)。
13.4 避免 N+1 查询
- 立即加载(Eager Loading) :通过
fetchType="eager"
或@BatchSize
批量加载关联数据。 - 嵌套查询优化 :将多个
SELECT
合并为一个JOIN
查询(通过association.fetchType="join"
)。
总结
MyBatis 作为优秀的持久层框架,通过灵活的 SQL 管理、强大的插件机制和高效的缓存策略,成为企业级开发的必备工具。深入理解其核心原理(如 Configuration
的加载、Executor
的执行、MappedStatement
的映射)和高级特性(如延迟加载、插件开发),能够帮助开发者在实际项目中充分发挥 MyBatis 的优势,同时避免常见陷阱(如 SQL 注入、N+1 查询)。结合 Spring 集成,MyBatis 能够与 Spring 生态无缝协作,提供更简洁的事务管理和更高效的开发体验。
😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 🤩 🥰 😘 😗 😙 😋 😛 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 😠 😤 😭
MyBatis 插件运行原理与编写详解
MyBatis 的插件机制是其高度可扩展的核心设计,允许开发者在不修改框架源码的情况下,通过拦截核心接口方法实现功能增强(如分页、日志、数据加密等)。以下从 运行原理 、拦截链机制 、插件编写步骤 到 实际案例 全面解析,确保深入理解。
一、插件运行原理:动态代理与拦截链
1. 核心拦截目标:四大接口
MyBatis 的插件仅能拦截以下四个核心接口的方法(覆盖 SQL 执行全流程):
Executor
:负责 SQL 的实际执行(增删改查),是插件的核心拦截点(如分页插件)。StatementHandler
:处理 JDBCStatement
的创建与参数设置(如 SQL 拼接、参数绑定)。ParameterHandler
:将 Java 对象转换为 JDBC 参数(如#{}
占位符赋值)。ResultSetHandler
:将 JDBCResultSet
转换为 Java 对象(如结果集映射)。
2. 动态代理实现拦截
MyBatis 通过 动态代理 为目标接口生成代理对象,代理逻辑由插件的 Interceptor
实现。具体流程如下:
2.1 代理创建
- 初始化阶段 :当
SqlSessionFactory
初始化时,扫描所有注册的插件,为被拦截的接口生成代理对象。 - 代理类型 :
- JDK 动态代理 :目标接口实现了
java.lang.reflect.InvocationHandler
(如Executor
接口)。 - CGLIB 代理 :目标接口未实现接口(如
StatementHandler
的实现类)。
- JDK 动态代理 :目标接口实现了
2.2 拦截逻辑触发
当业务代码调用目标接口方法(如 Executor.query()
)时,代理对象会拦截调用,并转发至插件的 intercept()
方法。具体步骤:
- 调用代理对象方法 :业务代码调用
executor.query(ms, parameter, rowBounds, resultHandler)
。 - 代理拦截 :代理对象捕获调用,创建
Invocation
对象(封装目标对象、方法、参数)。 - 执行拦截链 :调用
InterceptorChain.proceed()
,按顺序执行所有注册插件的intercept()
方法。 - 返回结果 :最后一个插件调用
invocation.proceed()
后,返回原方法或修改后的结果。
3. 拦截链(InterceptorChain)
MyBatis 支持多个插件协同工作,拦截器按 注册顺序 形成链式调用。例如:
java
// 配置多个插件(顺序决定执行顺序)
<plugins>
<plugin interceptor="com.example.LogPlugin"/> <!-- 日志插件 -->
<plugin interceptor="com.example.PagePlugin"/> <!-- 分页插件 -->
</plugins>
- 执行顺序 :
LogPlugin.intercept()
→PagePlugin.intercept()
→ 原方法。 - 关键机制 :每个插件的
intercept()
方法需显式调用invocation.proceed()
以传递控制权,否则后续插件和原方法不会执行。
二、插件编写全流程:从接口定义到注册
步骤 1:定义插件类并实现 Interceptor 接口
所有插件需实现 org.apache.ibatis.plugin.Interceptor
接口,核心是重写 intercept(Invocation invocation)
方法。
1.1 Interceptor 接口定义
java
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// 可选:读取配置文件中的属性
}
}
intercept()
:拦截逻辑的核心方法,接收Invocation
对象(封装目标对象、方法、参数)。plugin()
:生成代理对象的工具方法(默认实现使用Plugin.wrap()
)。setProperties()
:可选方法,用于从mybatis-config.xml
中读取插件配置属性。
1.2 实现拦截逻辑
以 分页插件 为例,演示如何拦截 Executor.query()
方法并修改 SQL:
java
public class PagePlugin implements Interceptor {
// 分页参数(示例:从 ThreadLocal 中获取)
private static final ThreadLocal<Page> PAGE_THREAD_LOCAL = new ThreadLocal<>();
// 设置分页参数(业务代码中调用)
public static void setPage(Page page) {
PAGE_THREAD_LOCAL.set(page);
}
// 清除分页参数
public static void clearPage() {
PAGE_THREAD_LOCAL.remove();
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取目标对象(如 Executor 实例)
Object target = invocation.getTarget();
// 2. 判断是否需要拦截(仅拦截 Executor.query())
if (target instanceof Executor && isQueryMethod(invocation)) {
// 3. 获取分页参数
Page page = PAGE_THREAD_LOCAL.get();
if (page != null && page.getPageSize() > 0) {
// 4. 修改 SQL:添加 LIMIT offset, pageSize
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
BoundSql boundSql = ms.getBoundSql(page.getParams());
String originalSql = boundSql.getSql();
String newSql = originalSql + " LIMIT " + page.getOffset() + ", " + page.getPageSize();
// 5. 替换 MappedStatement 的 SQL(生成新的 MappedStatement)
MappedStatement newMs = createNewMappedStatement(ms, newSql);
invocation.getArgs()[0] = newMs; // 替换目标对象
}
}
// 6. 继续执行原方法(或修改后的逻辑)
return invocation.proceed();
}
// 辅助方法:判断是否是 query 方法
private boolean isQueryMethod(Invocation invocation) {
try {
Method method = invocation.getMethod();
return method.getName().equals("query")
&& method.getParameterTypes().length == 4
&& MappedStatement.class.isAssignableFrom(method.getParameterTypes()[0]);
} catch (Exception e) {
return false;
}
}
// 辅助方法:生成新的 MappedStatement
private MappedStatement createNewMappedStatement(MappedStatement ms, String newSql) {
// 使用 MyBatis 内部工具类生成新的 MappedStatement
// 实际实现需处理参数映射、结果映射等(简化示例)
return new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), new StaticSqlSource(ms.getConfiguration(), newSql), ms.getSqlCommandType())
.resource(ms.getResource())
.parameterMap(ms.getParameterMap())
.resultMaps(ms.getResultMaps())
.build();
}
}
步骤 2:通过注解指定拦截目标
使用 @Intercepts
和 @Signature
注解明确拦截的接口和方法,避免拦截无关方法。
2.1 @Intercepts 与 @Signature 说明
@Intercepts
:声明插件要拦截的接口列表。@Signature
:声明拦截的具体方法(需指定接口类型、方法名、参数类型)。
java
@Intercepts({
// 拦截 Executor 接口的 query 方法(参数类型需完全匹配)
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
),
// 可选:拦截其他接口(如 StatementHandler)
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PagePlugin implements Interceptor {
// ... 插件逻辑 ...
}
- 参数类型匹配 :
args
需与目标方法的参数类型完全一致(如Executor.query()
的参数类型为MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class
)。
步骤 3:在 mybatis-config.xml 中注册插件
将插件配置到 MyBatis 的全局配置中,确保被扫描加载。
xml
<configuration>
<plugins>
<!-- 注册自定义插件(全限定类名) -->
<plugin interceptor="com.example.plugin.PagePlugin"/>
</plugins>
</configuration>
步骤 4:验证插件生效
通过业务代码验证插件是否拦截目标方法(如分页查询是否自动添加 LIMIT
)。
java
try (SqlSession session = sqlSessionFactory.openSession()) {
UserMapper mapper = session.getMapper(UserMapper.class);
// 设置分页参数(业务代码)
Page page = new Page(1, 10); // 第1页,每页10条
PagePlugin.setPage(page);
// 执行查询(自动触发分页插件)
List<User> users = mapper.selectAll();
// 清除分页参数(避免影响后续查询)
PagePlugin.clearPage();
}
三、插件的核心机制与高级特性
3.1 拦截链的执行顺序
MyBatis 按 插件注册顺序 执行拦截链。例如,注册顺序为 LogPlugin
→ PagePlugin
,则执行顺序为:
markdown
LogPlugin.intercept() → PagePlugin.intercept() → 原方法
- 调试技巧 :可通过日志输出拦截链顺序(在
intercept()
方法中打印插件名称)。
3.2 插件的线程安全
- 单例实例 :MyBatis 插件默认是单例的(
SqlSessionFactory
初始化时创建),需避免共享状态。 - 线程隔离 :若插件需存储线程相关数据(如分页参数),应使用
ThreadLocal
存储(如PagePlugin
示例中的PAGE_THREAD_LOCAL
)。
3.3 动态修改 SQL 与参数
插件可通过 BoundSql
对象动态修改 SQL 语句或参数(如分页、数据脱敏)。
java
// 获取 BoundSql 对象
BoundSql boundSql = ms.getBoundSql(parameterObject);
// 修改 SQL(添加注释)
String originalSql = boundSql.getSql();
String newSql = "/* 分页查询 */ " + originalSql;
boundSql.setSql(newSql); // 直接修改 SQL
// 修改参数(如替换敏感字段)
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (ParameterMapping mapping : parameterMappings) {
if ("password".equals(mapping.getProperty())) {
// 将密码参数替换为 ***
Object value = boundSql.getParameterObject(mapping.getProperty());
boundSql.setParameter(mapping.getProperty(), "***");
}
}
3.4 处理返回值
插件可修改方法的返回值(如结果集加密、日志记录)。
java
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed(); // 执行原方法获取结果
if (result instanceof List) {
// 对结果集进行脱敏处理(如隐藏手机号中间四位)
List<Object> list = (List<Object>) result;
for (Object obj : list) {
if (obj instanceof User) {
User user = (User) obj;
String phone = user.getPhone();
user.setPhone(phone.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
}
}
}
return result;
}
四、常见插件场景与实现
4.1 日志插件(记录 SQL 与参数)
- 目标 :拦截
StatementHandler.prepare()
方法,记录最终执行的 SQL 和参数。 - 实现:
java
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class LogPlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 获取 StatementHandler 实例
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 获取 BoundSql(包含 SQL 和参数)
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
List<ParameterMapping> mappings = boundSql.getParameterMappings();
// 替换占位符为实际参数值(用于日志)
Configuration configuration = handler.getConfiguration();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
Object parameterObject = boundSql.getParameterObject();
// 构建带参数的 SQL 日志
StringBuilder logSql = new StringBuilder(sql);
if (!mappings.isEmpty() && parameterObject != null) {
logSql.append(" [");
for (int i = 0; i < mappings.size(); i++) {
ParameterMapping mapping = mappings.get(i);
String propertyName = mapping.getProperty();
Object value = getParameterValue(parameterObject, propertyName);
TypeHandler<?> typeHandler = typeHandlerRegistry.getTypeHandler(mapping.getJavaType());
logSql.append(propertyName).append("=").append(typeHandler.toString(value));
if (i < mappings.size() - 1) {
logSql.append(", ");
}
}
logSql.append("]");
}
// 输出日志
System.out.println("执行的 SQL:" + logSql);
// 继续执行原方法
return invocation.proceed();
}
// 辅助方法:获取参数值
private Object getParameterValue(Object parameterObject, String propertyName) {
try {
if (parameterObject instanceof Map) {
return ((Map<?, ?>) parameterObject).get(propertyName);
} else {
return MetaObject.forObject(parameterObject).getValue(propertyName);
}
} catch (Exception e) {
return null;
}
}
}
4.2 性能监控插件(统计 SQL 执行耗时)
- 目标 :拦截
Executor.update()
和Executor.query()
方法,统计 SQL 执行时间。 - 实现:
java
@Intercepts({
@Signature(
type = Executor.class,
method = "update",
args = {MappedStatement.class, Object.class}
),
@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
)
})
public class PerformancePlugin implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
long startTime = System.currentTimeMillis();
try {
return invocation.proceed(); // 执行原方法
} finally {
long cost = System.currentTimeMillis() - startTime;
MappedStatement ms = (MappedStatement) invocation.getArgs()[0];
String sqlId = ms.getId();
System.out.println("SQL [" + sqlId + "] 执行耗时:" + cost + "ms");
}
}
}
五、插件开发的最佳实践
- 最小化拦截范围 :
仅拦截必要的方法(如Executor.query()
),避免过度拦截影响性能。 - 避免副作用 :
插件应保持无状态(或使用ThreadLocal
存储线程相关数据),避免修改全局状态。 - 性能优化 :
避免在intercept()
中执行耗时操作(如复杂计算),尽量减少对原方法逻辑的修改。 - 兼容性设计 :
插件需适配不同数据库(如 MySQL、Oracle 的分页语法差异),可通过DatabaseIdProvider
区分数据库类型。 - 配置外部化 :
插件参数(如分页大小、日志级别)应通过mybatis-config.xml
或@Properties
注入,提高灵活性。
总结
MyBatis 插件机制通过动态代理和拦截链实现了功能的灵活扩展,核心步骤为:
- 实现
Interceptor
接口并指定拦截目标(@Intercepts
和@Signature
); - 在
intercept()
方法中编写自定义逻辑(修改 SQL、参数或返回值); - 注册插件到
mybatis-config.xml
中。
合理使用插件可显著提升开发效率(如避免重复代码)和系统性能(如分页优化),但需注意线程安全和性能影响,确保插件与业务逻辑解耦。
😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 🤩 🥰 😘 😗 😙 😋 😛 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 😠 😤 😭