Mybatis:插件运行原理/延迟加载原理/二级缓存与二级缓存原理/接口绑定原理


1. MyBatis的插件运行原理,如何去编写一个插件?

插件运行原理

MyBatis 的插件机制基于拦截器(Interceptor) ,通过动态代理实现对核心组件的拦截。它允许开发者在特定执行点(Executor、StatementHandler、ParameterHandler、ResultSetHandler)插入自定义逻辑。插件的运行依赖于 MyBatis 的责任链模式。

  • 拦截对象 :MyBatis 提供了四种可拦截的核心对象:
    1. Executor:执行器,负责 SQL 执行和缓存管理。
    2. StatementHandler:语句处理器,负责 SQL 语句的预编译和执行。
    3. ParameterHandler:参数处理器,负责参数设置。
    4. ResultSetHandler:结果处理器,负责结果映射。
  • 运行流程
    1. MyBatis 在初始化时通过 Configuration 加载插件。
    2. 插件通过动态代理包装目标对象。
    3. 在目标方法执行时,调用插件的 intercept 方法插入自定义逻辑。

如何编写一个插件

编写插件需要实现 Interceptor 接口,并通过注解指定拦截目标。以下是一个简单的分页插件示例:

示例代码
java 复制代码
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;

@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SimplePagePlugin implements Interceptor {

    private int pageSize; // 每页大小
    private int pageNum;  // 当前页码

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        String sql = statementHandler.getBoundSql().getSql();
        // 修改 SQL,添加分页逻辑
        String pageSql = sql + " LIMIT " + (pageNum - 1) * pageSize + ", " + pageSize;
        // 通过反射修改 SQL
        Field field = statementHandler.getBoundSql().getClass().getDeclaredField("sql");
        field.setAccessible(true);
        field.set(statementHandler.getBoundSql(), pageSql);
        // 继续执行原方法
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        // 包装目标对象,只有符合拦截条件的对象才会被代理
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        // 从配置中获取参数
        this.pageSize = Integer.parseInt(properties.getProperty("pageSize", "10"));
        this.pageNum = Integer.parseInt(properties.getProperty("pageNum", "1"));
    }
}
配置插件

mybatis-config.xml 中注册:

xml 复制代码
<plugins>
    <plugin interceptor="com.example.SimplePagePlugin">
        <property name="pageSize" value="5"/>
        <property name="pageNum" value="1"/>
    </plugin>
</plugins>
原理分析
  1. @Intercepts@Signature 指定拦截的对象和方法。
  2. intercept 方法定义拦截逻辑,invocation.proceed() 调用原始方法。
  3. plugin 方法决定是否包装目标对象,使用 Plugin.wrap 生成代理。
  4. setProperties 用于接收配置参数。

总结

MyBatis 插件通过动态代理和责任链实现,编写时需明确拦截点并实现 Interceptor 接口,适合扩展分页、日志等功能。


2. MyBatis是否支持延迟加载,如果支持,原理是什么?

是否支持

MyBatis 支持延迟加载(Lazy Loading),但默认关闭,需要手动配置。

配置方式

mybatis-config.xml 中启用:

xml 复制代码
<settings>
    <setting name="lazyLoadingEnabled" value="true"/> <!-- 全局启用延迟加载 -->
    <setting name="aggressiveLazyLoading" value="false"/> <!-- 是否激进加载,默认 false -->
</settings>

原理

延迟加载依赖于 MyBatis 的动态代理结果映射机制。当查询主对象时,关联对象不会立即加载,而是生成一个代理对象,只有在首次访问关联对象时才触发加载。

  • 核心组件
    • ResultMap:定义关联关系。
    • ProxyFactory:生成代理对象(默认使用 Javassist 或 CGLIB)。
  • 执行流程
    1. 主查询执行,返回主对象。
    2. 关联对象字段被设置为代理对象。
    3. 访问关联对象时,代理触发子查询加载数据。

示例

假设 UserOrder 关联:

xml 复制代码
<resultMap id="userMap" type="User">
    <id property="id" column="id"/>
    <result property="name" column="name"/>
    <association property="order" column="order_id" javaType="Order" select="com.example.OrderMapper.selectOrderById"/>
</resultMap>

<select id="selectUser" resultMap="userMap">
    SELECT id, name, order_id FROM user WHERE id = #{id}
</select>

<select id="selectOrderById" resultType="Order">
    SELECT * FROM order WHERE id = #{id}
</select>

Java 代码:

java 复制代码
SqlSession session = sqlSessionFactory.openSession();
User user = session.selectOne("com.example.UserMapper.selectUser", 1);
System.out.println(user.getName()); // 主查询执行
System.out.println(user.getOrder().getOrderNo()); // 子查询触发

原理分析

  • lazyLoadingEnabled=true 时,MyBatis 为 order 属性生成代理。
  • 访问 getOrder() 时,代理调用 selectOrderById 查询数据库。

总结

MyBatis 通过代理实现延迟加载,适用于减少初始查询开销,但需注意 N+1 问题(多次子查询)。


3. 详细分析一级缓存与二级缓存

一级缓存

  • 作用范围:SqlSession 级别,默认开启。
  • 实现原理
    • 使用 PerpetualCache(基于 HashMap)存储,位于 BaseExecutor 中。
    • 键由 MappedStatement ID + 参数 + SQL 组成,值是查询结果。
  • 生命周期
    • SqlSession 创建时初始化,关闭时销毁。
    • 增删改操作或调用 clearCache() 会清空。
  • 代码示例
java 复制代码
SqlSession session = sqlSessionFactory.openSession();
User user1 = session.selectOne("com.example.UserMapper.selectUser", 1); // 查询数据库
User user2 = session.selectOne("com.example.UserMapper.selectUser", 1); // 缓存命中
session.close();

二级缓存

  • 作用范围:Mapper 级别,跨 SqlSession,需手动开启。

  • 实现原理

    • 使用 Cache 接口,默认实现为 PerpetualCache,存储在 Configurationcaches 中。
    • 可集成第三方缓存(如 Ehcache)。
  • 配置方式

    xml 复制代码
    <settings>
        <setting name="cacheEnabled" value="true"/>
    </settings>
    <mapper namespace="com.example.UserMapper">
        <cache/>
    </mapper>
  • 生命周期

    • 跟随 Mapper 生命周期,增删改操作清空对应缓存。
  • 代码示例

java 复制代码
SqlSession session1 = sqlSessionFactory.openSession();
User user1 = session1.selectOne("com.example.UserMapper.selectUser", 1); // 查询数据库
session1.close();

SqlSession session2 = sqlSessionFactory.openSession();
User user2 = session2.selectOne("com.example.UserMapper.selectUser", 1); // 缓存命中
session2.close();

对比分析

特性 一级缓存 二级缓存
作用范围 SqlSession Mapper
默认状态 开启 关闭
存储位置 BaseExecutor Configuration
清空条件 增删改、关闭 session 增删改
配置复杂度 无需配置 需要手动配置

总结

  • 一级缓存简单高效,适合单次会话。
  • 二级缓存跨会话共享,适合读多写少场景,但需注意一致性问题。

4. 简述MyBatis的接口绑定原理

接口绑定原理

MyBatis 的接口绑定通过动态代理实现,将 Mapper 接口与 XML 或注解中的 SQL 绑定,无需手动实现接口。

  • 核心组件
    • MapperProxy:动态代理类。
    • MapperRegistry:注册和管理 Mapper 接口。
  • 执行流程
    1. Configuration 初始化时,解析 Mapper 接口和对应的 XML 文件。
    2. 使用 MapperProxyFactory 为接口生成代理对象。
    3. 调用接口方法时,代理对象根据方法名和命名空间定位 MappedStatement,执行 SQL。

示例

接口:

java 复制代码
public interface UserMapper {
    User selectUser(int id);
}

XML:

xml 复制代码
<mapper namespace="com.example.UserMapper">
    <select id="selectUser" resultType="User">
        SELECT * FROM user WHERE id = #{id}
    </select>
</mapper>

使用:

java 复制代码
SqlSession session = sqlSessionFactory.openSession();
UserMapper mapper = session.getMapper(UserMapper.class);
User user = mapper.selectUser(1); // 代理执行 SQL

源码分析

getMapper 方法:

java 复制代码
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

代理生成:

java 复制代码
public class MapperProxy<T> implements InvocationHandler {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 根据方法名和参数执行对应的 MappedStatement
        return mapperMethod.execute(sqlSession, args);
    }
}

总结

MyBatis 通过动态代理将接口方法与 SQL 绑定,简化开发,提高灵活性。


相关推荐
uzong5 小时前
技术故障复盘模版
后端
GetcharZp6 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程6 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研6 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi7 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国8 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy8 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack8 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9659 小时前
pip install 已经不再安全
后端
寻月隐君9 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github