如何编写MyBatis插件:延迟加载、缓存与接口绑定原理
MyBatis是一款非常受欢迎的持久层框架,今天咱们就深入探讨下MyBatis里几个关键特性的原理,包括插件运行原理、延迟加载原理、一级缓存与二级缓存原理,还有接口绑定原理,顺便也讲讲怎么编写MyBatis插件。
一、MyBatis插件运行原理与编写方法
(一)插件运行原理
MyBatis的插件机制很巧妙,它是基于拦截器(Interceptor)来实现的,利用动态代理对核心组件进行拦截。通过这个机制,开发者能在特定的执行点,比如执行器(Executor)、语句处理器(StatementHandler)、参数处理器(ParameterHandler)、结果处理器(ResultSetHandler)这些地方,插入自己定义的逻辑。而且,插件的运行还依赖于MyBatis的责任链模式。
MyBatis提供了四种可以拦截的核心对象:
- Executor(执行器):主要负责SQL语句的执行,同时还管理着缓存。
- StatementHandler(语句处理器):它的任务是对SQL语句进行预编译,然后执行这些语句。
- ParameterHandler(参数处理器):负责给SQL语句设置参数。
- ResultSetHandler(结果处理器):将查询结果进行映射处理。
MyBatis插件的运行流程大概是这样的:
- 在MyBatis初始化的时候,会通过Configuration加载插件。
- 插件会通过动态代理的方式,把目标对象包装起来。
- 当目标方法执行的时候,就会调用插件的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。
执行流程如下:
- 执行主查询,返回主对象。
- 关联对象的字段会被设置为代理对象。
- 当访问关联对象时,代理对象会触发子查询来加载数据。
假设User
和Order
有关联关系,示例代码如下:
<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缓存:一级缓存与二级缓存
(一)一级缓存
- 作用范围:一级缓存的作用范围是SqlSession级别,默认是开启的。
- 实现原理:它使用
PerpetualCache
(基于HashMap
)来存储数据,位于BaseExecutor
中。缓存的键由MappedStatement ID + 参数 + SQL
组成,对应的值就是查询结果。 - 生命周期:在SqlSession创建的时候初始化,关闭SqlSession时销毁。另外,执行增删改操作或者调用
clearCache()
方法,都会清空一级缓存。 - 代码示例:
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
,存储在Configuration
的caches
中。而且,还能集成第三方缓存,比如Ehcache。 - 配置方式:
<settings> <setting name="cacheEnabled" value="true"/> </settings> <mapper namespace="com.example.UserMapper"> <cache/> </mapper>
- 生命周期:二级缓存跟随Mapper的生命周期,执行增删改操作会清空对应Mapper的缓存。
- 代码示例:
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接口。
执行流程如下:
- 在
Configuration
初始化的时候,会解析Mapper接口和对应的XML文件。 - 使用
MapperProxyFactory
为接口生成代理对象。 - 调用接口方法时,代理对象会根据方法名和命名空间定位
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通过动态代理实现接口绑定,简化了开发过程,提高了开发的灵活性。