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 绑定,简化开发,提高灵活性。


相关推荐
EngineerSuTao5 分钟前
Flask的app.run()里发生了什么
后端·python·flask
Asthenia04126 分钟前
进程与线程区别 / Java线程状态 / 同步方法与代码块差异 / Monitor线程同步原理 / 死锁解析 / 多线程访问资源避免死锁
后端
Asthenia041225 分钟前
模块引用/ pass/ 模块包/ range/xrange/ dict/items/ is/ func引用/ any/all/ 列表值/ 排序
后端
计算机学姐30 分钟前
基于SpringBoot的名著阅读网站
java·vue.js·spring boot·后端·mysql·spring·intellij-idea
努力的小雨31 分钟前
Spring AI 增加混元 embedding 向量功能
后端
慕容靖翾35 分钟前
CSS语言的排序算法
开发语言·后端·golang
Asthenia04121 小时前
装饰器 / Lambda / Tuple-List / except / match-search / 全局变量 / 引号 / 参数 / 类-实例变量
后端
阳光照进我心窝1 小时前
Laravel 模型静态方法实现
后端
喵个咪1 小时前
Ent代码生成工具链
后端·go
SimonKing1 小时前
XXL-JOB:揭秘定时机制
java·后端·架构