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