1. MyBatis的插件运行原理,如何去编写一个插件?
插件运行原理
MyBatis 的插件机制基于拦截器(Interceptor) ,通过动态代理实现对核心组件的拦截。它允许开发者在特定执行点(Executor、StatementHandler、ParameterHandler、ResultSetHandler)插入自定义逻辑。插件的运行依赖于 MyBatis 的责任链模式。
- 拦截对象 :MyBatis 提供了四种可拦截的核心对象:
Executor
:执行器,负责 SQL 执行和缓存管理。StatementHandler
:语句处理器,负责 SQL 语句的预编译和执行。ParameterHandler
:参数处理器,负责参数设置。ResultSetHandler
:结果处理器,负责结果映射。
- 运行流程 :
- MyBatis 在初始化时通过
Configuration
加载插件。 - 插件通过动态代理包装目标对象。
- 在目标方法执行时,调用插件的
intercept
方法插入自定义逻辑。
- MyBatis 在初始化时通过
如何编写一个插件
编写插件需要实现 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>
原理分析
@Intercepts
和@Signature
指定拦截的对象和方法。intercept
方法定义拦截逻辑,invocation.proceed()
调用原始方法。plugin
方法决定是否包装目标对象,使用Plugin.wrap
生成代理。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)。
- 执行流程 :
- 主查询执行,返回主对象。
- 关联对象字段被设置为代理对象。
- 访问关联对象时,代理触发子查询加载数据。
示例
假设 User
有 Order
关联:
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
,存储在Configuration
的caches
中。 - 可集成第三方缓存(如 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 接口。
- 执行流程 :
- 在
Configuration
初始化时,解析 Mapper 接口和对应的 XML 文件。 - 使用
MapperProxyFactory
为接口生成代理对象。 - 调用接口方法时,代理对象根据方法名和命名空间定位
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 绑定,简化开发,提高灵活性。