MyBatis 核心知识点、插件

😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 🤩 🥰 😘 😗 😙 😋 😛 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 😠 😤 😭

MyBatis 核心知识点详解


一、配置加载与初始化:从 XML 到 Configuration 对象

MyBatis 的初始化流程始于 SqlSessionFactoryBuilder 对配置文件的解析,最终生成全局唯一的 SqlSessionFactory。这一过程涉及复杂的 XML 解析、属性注入和对象创建。

1.1 配置文件解析流程
  • 入口类SqlSessionFactoryBuilderbuild(InputStream inputStream) 方法。
  • 解析步骤
    1. 读取 mybatis-config.xml :使用 XMLConfigBuilder 解析根节点 <configuration>
    2. 解析子节点
      • <properties>:加载外部属性文件(如 db.properties),支持 ${} 占位符替换。
      • <settings>:配置 MyBatis 核心参数(如 cacheEnabledlazyLoadingEnableddefaultExecutorType),通过 Settings 对象存储。
      • <typeAliases>:定义类型别名(如 typeAlias="User"),简化 parameterTyperesultType 的书写。
      • <environments>:配置数据库环境(development/production),包含 dataSource(数据源)和 transactionManager(事务管理器)。
      • <mappers>:注册 Mapper 接口或 XML 映射文件(支持 resourcepackageclass 三种方式)。
    3. 构建 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()
  • 核心逻辑
    1. 确定事务隔离级别 :默认 Connection.TRANSACTION_READ_COMMITTED(可读已提交)。
    2. 创建 Executor :根据 Configuration.defaultExecutorType(默认 SIMPLE)选择执行器类型。
    3. 生成 DefaultSqlSession :持有 ExecutorConfiguration 和事务状态(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确保及时关闭:
    java 复制代码
    try (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 通过 ConnectionsetAutoCommit(false) 开启事务,commit()/rollback() 提交/回滚。
  • 连接管理ExecutorDataSource 获取连接(支持连接池),并在 SqlSession 关闭时释放。

四、MappedStatement:SQL 的元数据封装

MappedStatement 是 MyBatis 中 SQL 语句的元数据对象,封装了 SQL 模板、参数类型、结果类型等关键信息。

4.1 MappedStatement 的生成流程
  1. 解析映射文件XMLMapperBuilder 解析 <select>, <insert>, <update>, <delete> 标签。
  2. 构建 BoundSql :将 SQL 模板(含动态标签)转换为 BoundSql 对象,解析参数占位符(#{})和结果映射。
  3. 注册到 Configuration :将 MappedStatement 存入 Configuration.mappedStatements(键为 namespace.id)。
4.2 MappedStatement 的核心属性
属性 说明
id SQL 语句的唯一标识(对应 Mapper 接口方法名)。
parameterType 参数类型(Java 类或别名,如 Userjava.lang.String)。
resultType 结果类型(Java 类或别名,如 User)。
resultMap 自定义结果映射(解决字段名与属性名不一致问题)。
sqlSource SQL 源(StaticSqlSourceDynamicSqlSource,动态 SQL 用后者)。
keyGenerator 主键生成策略(Jdbc3KeyGeneratorSelectKeyGenerator)。
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 的执行流程
  1. 获取参数对象 :从 MappedStatement 中获取参数类型(parameterType)。
  2. 创建 TypeHandler :根据参数类型选择对应的 TypeHandler(如 StringTypeHandlerIntegerTypeHandler)。
  3. 设置参数值 :调用 TypeHandler.setParameter(PreparedStatement, index, value, jdbcType) 方法。
5.2 复杂参数的处理
  • POJO 对象 :通过 OGNL 表达式提取属性值(如 #{user.name} 提取 user 对象的 name 属性)。
  • 集合类型 :支持 ListArray 等,通过 #{list[0]} 访问元素(需配合 <foreach> 标签)。
  • 命名参数 :通过 @Param 注解指定参数名(如 @Param("userName") String name),#{userName} 直接引用。
5.3 TypeHandler 的自定义
  • 实现 TypeHandler 接口:

    java 复制代码
    public 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 的执行流程
  1. 获取 ResultSet :通过 Statement.executeQuery() 获取结果集。
  2. 创建 ResultLoader :处理嵌套查询(associationcollection)。
  3. 映射结果 :根据 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)拦截四大核心接口(ExecutorStatementHandlerParameterHandlerResultSetHandler)的方法,实现功能扩展(如分页、日志)。

7.1 插件的注册与执行
  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(); // 继续执行原方法
  }
}
  1. 注册插件 :在 mybatis-config.xml 中配置:
xml 复制代码
<plugins>
  <plugin interceptor="com.example.PagePlugin"/>
</plugins>
7.2 拦截器的链式调用
  • InterceptorChain :管理所有插件的拦截顺序,通过 pluginAll() 方法为目标对象生成代理。
  • 代理类型
    • JDK 动态代理 :目标对象实现了接口(如 Executor 接口)。
    • CGLIB 代理 :目标对象未实现接口(如 StatementHandler 的实现类)。
7.3 分页插件原理(以 PageHelper 为例)
  • 拦截 Executor.query() :解析分页参数(pageNumpageSize)。
  • 修改 SQL :将原始 SQL 包裹为 SELECT * FROM (原 SQL) LIMIT offset, pageSize(MySQL)或 ROWNUM(Oracle)。
  • 缓存总记录数 :通过 COUNT(*) 查询获取总页数,填充到 Page 对象。

八、事务管理:从 JDBC 到 Spring 集成

MyBatis 的事务管理基于 JDBC 事务,支持手动提交和声明式事务(与 Spring 集成时)。

8.1 事务的生命周期
  1. 开启事务SqlSession 初始化时,通过 Connection.setAutoCommit(false) 开启事务。
  2. 执行操作 :执行增删改查,结果暂存于 Connection
  3. 提交/回滚session.commit() 调用 Connection.commit()session.rollback() 调用 Connection.rollback()
  4. 关闭连接session.close() 释放连接回连接池。
8.2 与 Spring 的集成
  • SqlSessionTemplate :Spring 提供的 SqlSession 包装类,解决线程安全问题(通过 ThreadLocal 绑定当前线程的 SqlSession)。
  • MapperFactoryBean :将 Mapper 接口注册为 Spring Bean,通过 proxyFactory 生成代理对象。
  • 声明式事务 :通过 @Transactional 注解,由 PlatformTransactionManager 管理 SqlSession 的提交/回滚。
8.3 事务隔离级别
  • 支持级别READ_UNCOMMITTEDREAD_COMMITTEDREPEATABLE_READSERIALIZABLE(通过 transactionManager 配置)。
  • 默认级别READ_COMMITTED(与大多数数据库默认一致)。

九、缓存机制:提升查询性能

MyBatis 提供一级缓存(SqlSession 级别)和二级缓存(Mapper 级别),减少数据库访问次数。

9.1 一级缓存
  • 作用域 :与 SqlSession 绑定,SqlSession 关闭后失效。
  • 存储结构PerpetualCache(基于 HashMap)。
  • 失效场景
    • 执行 updateinsertdelete 操作(默认清空缓存)。
    • 手动调用 session.clearCache()
  • 配置 :默认开启,无法关闭(可通过 localCacheScope=STATEMENT 禁用)。
9.2 二级缓存
  • 作用域 :与 Mappernamespace)绑定,多个 SqlSession 共享。

  • 启用条件

    • 全局配置 mybatis-config.xmlcacheEnabled=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 执行了写操作(updateinsertdelete),二级缓存会被清空。
  • 手动刷新 :调用 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 的执行流程
  1. 解析 XML 映射文件XMLMapperBuilder<select> 标签转换为 MappedStatement
  2. 生成 BoundSqlDynamicSqlSource 解析 SqlNode 树,生成最终的 SQL 字符串和参数映射。
  3. 执行 SQLExecutor 使用 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 集成后,通过 SqlSessionTemplateMapperScannerConfigurer 简化开发,实现自动事务管理。

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 使用 SqlSessionTemplategetConnection() 获取数据库连接,实现事务同步。

十三、性能优化与最佳实践

13.1 减少数据库交互
  • 批量操作 :使用 BATCH 执行器或 <foreach> 标签减少 INSERT/UPDATE 次数。
  • 缓存优化:高频读、低频写场景启用二级缓存;写频繁场景关闭缓存。
13.2 SQL 优化
  • 索引优化WHEREJOINORDER BY 字段添加索引。
  • 避免 SELECT *:明确指定需要的字段,减少数据传输量。
  • 分页查询 :使用 LIMITROWNUM 限制返回行数,避免全表扫描。
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 :处理 JDBC Statement 的创建与参数设置(如 SQL 拼接、参数绑定)。
  • ParameterHandler :将 Java 对象转换为 JDBC 参数(如 #{} 占位符赋值)。
  • ResultSetHandler :将 JDBC ResultSet 转换为 Java 对象(如结果集映射)。
2. 动态代理实现拦截

MyBatis 通过 动态代理 为目标接口生成代理对象,代理逻辑由插件的 Interceptor 实现。具体流程如下:

2.1 代理创建
  • 初始化阶段 :当 SqlSessionFactory 初始化时,扫描所有注册的插件,为被拦截的接口生成代理对象。
  • 代理类型
    • JDK 动态代理 :目标接口实现了 java.lang.reflect.InvocationHandler(如 Executor 接口)。
    • CGLIB 代理 :目标接口未实现接口(如 StatementHandler 的实现类)。
2.2 拦截逻辑触发

当业务代码调用目标接口方法(如 Executor.query())时,代理对象会拦截调用,并转发至插件的 intercept() 方法。具体步骤:

  1. 调用代理对象方法 :业务代码调用 executor.query(ms, parameter, rowBounds, resultHandler)
  2. 代理拦截 :代理对象捕获调用,创建 Invocation 对象(封装目标对象、方法、参数)。
  3. 执行拦截链 :调用 InterceptorChain.proceed(),按顺序执行所有注册插件的 intercept() 方法。
  4. 返回结果 :最后一个插件调用 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 按 插件注册顺序 执行拦截链。例如,注册顺序为 LogPluginPagePlugin,则执行顺序为:

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");
        }
    }
}

五、插件开发的最佳实践

  1. 最小化拦截范围
    仅拦截必要的方法(如 Executor.query()),避免过度拦截影响性能。
  2. 避免副作用
    插件应保持无状态(或使用 ThreadLocal 存储线程相关数据),避免修改全局状态。
  3. 性能优化
    避免在 intercept() 中执行耗时操作(如复杂计算),尽量减少对原方法逻辑的修改。
  4. 兼容性设计
    插件需适配不同数据库(如 MySQL、Oracle 的分页语法差异),可通过 DatabaseIdProvider 区分数据库类型。
  5. 配置外部化
    插件参数(如分页大小、日志级别)应通过 mybatis-config.xml@Properties 注入,提高灵活性。

总结

MyBatis 插件机制通过动态代理和拦截链实现了功能的灵活扩展,核心步骤为:

  1. 实现 Interceptor 接口并指定拦截目标(@Intercepts@Signature);
  2. intercept() 方法中编写自定义逻辑(修改 SQL、参数或返回值);
  3. 注册插件到 mybatis-config.xml 中。

合理使用插件可显著提升开发效率(如避免重复代码)和系统性能(如分页优化),但需注意线程安全和性能影响,确保插件与业务逻辑解耦。

😄 😁 😆 😅 😂 🤣 😊 😇 🙂 🙃 😉 😌 😍 🤩 🥰 😘 😗 😙 😋 😛 🤩 🥳 😏 😒 😞 😔 😟 😕 🙁 😠 😤 😭

相关推荐
come112342 分钟前
Go 包管理工具详解:安装与使用指南
开发语言·后端·golang
笑衬人心。8 分钟前
Hashtable 与 HashMap 的区别笔记
java·数据结构·笔记
金心靖晨10 分钟前
消息中间件优化高手笔记
java·数据库·笔记
CV练习生Zzz42 分钟前
MATLAB知识点总结
开发语言·matlab
深度混淆1 小时前
C#,Parallel并行多线程计算,使用专门的Concurrent系列数据集
开发语言·c#·多线程·并行处理
每一天都要努力^1 小时前
C++函数指针
开发语言·c++
刚入门的大一新生1 小时前
C++进阶-多态2
开发语言·c++
lemon_sjdk1 小时前
Java飞机大战小游戏(升级版)
java·前端·python
LUCIAZZZ1 小时前
高性能网络模式-Reactor和Preactor
java·服务器·开发语言·网络·操作系统·计算机系统
Dcs2 小时前
Java 开发者必读:近期框架更新汇总(Spring gRPC、Micronaut、Quarkus 等)
java