MyBatis 核心知识点、插件

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

MyBatis 核心知识点详解


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

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

1.1 配置文件解析流程
  • 入口类 :SqlSessionFactoryBuilder 的 build(InputStream inputStream) 方法。
  • 解析步骤 :
    1. 读取 mybatis-config.xml :使用 XMLConfigBuilder 解析根节点 <configuration>。
    2. 解析子节点 :
      • <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 三种方式)。
    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 :持有 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确保及时关闭:
    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 通过 Connection 的 setAutoCommit(false) 开启事务,commit()/rollback() 提交/回滚。
  • 连接管理 :Executor 从 DataSource 获取连接(支持连接池),并在 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 类或别名,如 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 的执行流程
  1. 获取参数对象 :从 MappedStatement 中获取参数类型(parameterType)。
  2. 创建 TypeHandler :根据参数类型选择对应的 TypeHandler(如 StringTypeHandler、IntegerTypeHandler)。
  3. 设置参数值 :调用 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 接口:

    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 :处理嵌套查询(association、collection)。
  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)拦截四大核心接口(Executor、StatementHandler、ParameterHandler、ResultSetHandler)的方法,实现功能扩展(如分页、日志)。

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() :解析分页参数(pageNum、pageSize)。
  • 修改 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_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 的执行流程
  1. 解析 XML 映射文件 :XMLMapperBuilder 将 <select> 标签转换为 MappedStatement。
  2. 生成 BoundSql :DynamicSqlSource 解析 SqlNode 树,生成最终的 SQL 字符串和参数映射。
  3. 执行 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 :处理 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 按 插件注册顺序 执行拦截链。例如,注册顺序为 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");
        }
    }
}

五、插件开发的最佳实践

  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 中。

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

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

相关推荐
Raas1006 分钟前
AI网关能做语义缓存吗?MAI Gateway(魔芋企业级AI网关)实战能力深度解读
java·后端·spring
weixin199701080168 分钟前
《1688消息服务落地方案:alibaba.message.* 与 Webhook 回调的可靠性与重试策略》(附Python源码)
开发语言·python
天空鸟_时光不老13 分钟前
13-亮点提炼:把技术说辞翻译成业务价值
java·spring boot·spring·spring cloud·mybatis
小蒜学长17 分钟前
基于SpringBoot+Vue的租房管理系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·租房管理
ZHOUPUYU17 分钟前
PHP 9.0 前瞻:下一代 PHP 会带来哪些新功能?
android·开发语言·php
某不知名網友18 分钟前
ROS2 核心通信:话题发布与订阅详解
开发语言·c++
C++ 老炮儿的技术栈19 分钟前
char (*csConnectName)[256]; 和 char csConnectName [256] 有什么区别
java·c语言·开发语言·c++·人工智能·算法·c
洋就在江州23 分钟前
gitlab-cicd 离线集成——springboot-cicd (非docker形式,shell形式)
java·spring boot·后端·ci/cd·gitlab·gitlab-runner
CEZ30 分钟前
JeeWMS 开源 WMS 全景导览:一套 Java 仓库管理系统从单据流到代码入口的阅读地图
java·开源
Nebula_g30 分钟前
JavaSE加强:线程Thread(线程安全重点)
java·开发语言·jvm·算法·javase·技术栈