如何编写MyBatis插件:延迟加载、缓存与接口绑定原理

后端 潘老师 1个月前 (03-23) 25 ℃ (0) 扫码查看

MyBatis是一款非常受欢迎的持久层框架,今天咱们就深入探讨下MyBatis里几个关键特性的原理,包括插件运行原理、延迟加载原理、一级缓存与二级缓存原理,还有接口绑定原理,顺便也讲讲怎么编写MyBatis插件。

一、MyBatis插件运行原理与编写方法

(一)插件运行原理

MyBatis的插件机制很巧妙,它是基于拦截器(Interceptor)来实现的,利用动态代理对核心组件进行拦截。通过这个机制,开发者能在特定的执行点,比如执行器(Executor)、语句处理器(StatementHandler)、参数处理器(ParameterHandler)、结果处理器(ResultSetHandler)这些地方,插入自己定义的逻辑。而且,插件的运行还依赖于MyBatis的责任链模式。

MyBatis提供了四种可以拦截的核心对象:

  • Executor(执行器):主要负责SQL语句的执行,同时还管理着缓存。
  • StatementHandler(语句处理器):它的任务是对SQL语句进行预编译,然后执行这些语句。
  • ParameterHandler(参数处理器):负责给SQL语句设置参数。
  • ResultSetHandler(结果处理器):将查询结果进行映射处理。

MyBatis插件的运行流程大概是这样的:

  1. 在MyBatis初始化的时候,会通过Configuration加载插件。
  2. 插件会通过动态代理的方式,把目标对象包装起来。
  3. 当目标方法执行的时候,就会调用插件的intercept方法,这时候咱们自定义的逻辑就能派上用场了。

(二)如何编写一个插件

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

import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.sql.Connection;
import java.util.Properties;

// 使用@Intercepts和@Signature注解指定拦截的对象和方法
@Intercepts({
    @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SimplePagePlugin implements Interceptor {
    // 定义每页大小和当前页码
    private int pageSize; 
    private int pageNum;  

    // 实现intercept方法,编写拦截逻辑
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 获取被代理的StatementHandler对象
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        // 获取原始SQL语句
        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文件里注册插件:

<plugins>
    <plugin interceptor="com.example.SimplePagePlugin">
        <property name="pageSize" value="5"/>
        <property name="pageNum" value="1"/>
    </plugin>
</plugins>

这里简单分析下代码原理:

  • @Intercepts@Signature用来明确指定要拦截的对象和方法。
  • intercept方法里写的就是具体的拦截逻辑,invocation.proceed()表示调用原始方法。
  • plugin方法决定是否对目标对象进行包装,用Plugin.wrap生成代理。
  • setProperties方法用来接收配置文件里的参数。

总的来说,MyBatis插件通过动态代理和责任链实现功能扩展,编写插件时要清楚拦截点,实现Interceptor接口,像分页、日志这些功能都能用插件来实现。

二、MyBatis延迟加载

(一)是否支持延迟加载

MyBatis是支持延迟加载(Lazy Loading)的,不过默认是关闭状态,需要手动配置才能开启。

(二)配置方式

mybatis-config.xml文件里进行如下配置:

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

(三)延迟加载原理

MyBatis的延迟加载依赖于动态代理和结果映射机制。当执行查询主对象的操作时,与之关联的对象并不会马上加载,而是生成一个代理对象。只有在首次访问这个关联对象的时候,才会真正触发加载操作。

这里面有两个核心组件:

  • ResultMap:主要用来定义对象之间的关联关系。
  • ProxyFactory:负责生成代理对象,默认使用Javassist或CGLIB。

执行流程如下:

  1. 执行主查询,返回主对象。
  2. 关联对象的字段会被设置为代理对象。
  3. 当访问关联对象时,代理对象会触发子查询来加载数据。

假设UserOrder有关联关系,示例代码如下:

<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>
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去查询数据库。

不过要注意,虽然延迟加载能减少初始查询的开销,但可能会出现N+1问题,也就是多次执行子查询。

三、MyBatis缓存:一级缓存与二级缓存

(一)一级缓存

  1. 作用范围:一级缓存的作用范围是SqlSession级别,默认是开启的。
  2. 实现原理:它使用PerpetualCache(基于HashMap)来存储数据,位于BaseExecutor中。缓存的键由MappedStatement ID + 参数 + SQL组成,对应的值就是查询结果。
  3. 生命周期:在SqlSession创建的时候初始化,关闭SqlSession时销毁。另外,执行增删改操作或者调用clearCache()方法,都会清空一级缓存。
  4. 代码示例
SqlSession session = sqlSessionFactory.openSession();
User user1 = session.selectOne("com.example.UserMapper.selectUser", 1); // 查询数据库
User user2 = session.selectOne("com.example.UserMapper.selectUser", 1); // 缓存命中
session.close();

(二)二级缓存

  1. 作用范围:二级缓存的作用范围是Mapper级别,它可以跨SqlSession共享数据,不过需要手动开启。
  2. 实现原理:二级缓存使用Cache接口,默认实现也是PerpetualCache,存储在Configurationcaches中。而且,还能集成第三方缓存,比如Ehcache。
  3. 配置方式
<settings>
    <setting name="cacheEnabled" value="true"/>
</settings>
<mapper namespace="com.example.UserMapper">
    <cache/>
</mapper>
  1. 生命周期:二级缓存跟随Mapper的生命周期,执行增删改操作会清空对应Mapper的缓存。
  2. 代码示例
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 增删改
配置复杂度 无需配置 需要手动配置

总的来说,一级缓存简单高效,适合在单次会话中使用;二级缓存能跨会话共享,在读取操作多、写入操作少的场景下很适用,但要注意数据一致性的问题。

四、MyBatis接口绑定:原理与示例

(一)接口绑定原理

MyBatis的接口绑定是通过动态代理实现的,它能把Mapper接口和XML文件或者注解里的SQL语句绑定起来,这样咱们就不用手动去实现接口了。

这里面的核心组件有:

  • MapperProxy:动态代理类。
  • MapperRegistry:负责注册和管理Mapper接口。

执行流程如下:

  1. Configuration初始化的时候,会解析Mapper接口和对应的XML文件。
  2. 使用MapperProxyFactory为接口生成代理对象。
  3. 调用接口方法时,代理对象会根据方法名和命名空间定位MappedStatement,然后执行对应的SQL语句。

(二)示例

定义接口:

public interface UserMapper {
    User selectUser(int id);
}

编写XML文件:

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

使用示例:

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

(三)源码分析

getMapper方法:

public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

代理生成:

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通过动态代理实现接口绑定,简化了开发过程,提高了开发的灵活性。


版权声明:本站文章,如无说明,均为本站原创,转载请注明文章来源。如有侵权,请联系博主删除。
本文链接:https://www.panziye.com/back/16178.html
喜欢 (0)
请潘老师喝杯Coffee吧!】
分享 (0)
用户头像
发表我的评论
取消评论
表情 贴图 签到 代码

Hi,您需要填写昵称和邮箱!

  • 昵称【必填】
  • 邮箱【必填】
  • 网址【可选】