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


相关推荐
我的ID配享太庙呀17 分钟前
Django 科普介绍:从入门到了解其核心魅力
数据库·后端·python·mysql·django·sqlite
java叶新东老师1 小时前
goland编写go语言导入自定义包出现: package xxx is not in GOROOT (/xxx/xxx) 的解决方案
开发语言·后端·golang
码事漫谈2 小时前
C++模板元编程从入门到精通
后端
_風箏2 小时前
Java【代码 14】一个用于判断磁盘空间和分区表是否需要清理的工具类
后端
_風箏2 小时前
Java【代码 13】前端动态添加一条记后端使用JDK1.8实现map对象根据key的部分值进行分组(将map对象封装成指定entity对象)
后端
_風箏3 小时前
Java【代码 12】判断一个集合是否包含另一个集合中的一个或多个元素 retainAll() 及其他方法
后端
Java中文社群3 小时前
Coze开源版?别吹了!
人工智能·后端·开源
懂得节能嘛.3 小时前
【SpringAI实战】ChatPDF实现RAG知识库
java·后端·spring
站大爷IP3 小时前
Python爬虫库性能与选型实战指南:从需求到落地的全链路解析
后端
小杰来搬砖3 小时前
在 Java 的 MyBatis 框架中,# 和 $ 的区别
后端