源码分析与面试题
2026/1/15大约 10 分钟MyBatis源码分析面试题
MyBatis源码分析与面试题
一、MyBatis整体架构
架构分层
核心组件职责
| 组件 | 职责 |
|---|---|
| SqlSessionFactory | 创建 SqlSession 的工厂 |
| SqlSession | 执行 SQL 的会话,门面模式 |
| Executor | SQL 执行器,真正执行 SQL |
| StatementHandler | 处理 JDBC Statement |
| ParameterHandler | 处理参数设置 |
| ResultSetHandler | 处理结果集映射 |
| TypeHandler | 类型转换器 |
二、SqlSession执行流程
🔥 面试题:MyBatis 执行 SQL 的完整流程是什么?
📝 面试回答要点
标准回答:
- 通过
SqlSession.getMapper()获取 Mapper 代理对象- 调用 Mapper 方法时,代理对象拦截调用,委托给
SqlSessionSqlSession委托给Executor执行Executor先查询缓存,未命中则创建StatementHandlerParameterHandler设置参数,StatementHandler执行 SQLResultSetHandler处理结果集映射- 结果写入缓存并返回
三、Mapper代理机制
🔥 面试题:Mapper 接口没有实现类,MyBatis 是如何执行的?
原理图解
核心源码(伪代码)
// MapperProxy - Mapper代理类
public class MapperProxy implements InvocationHandler {
private final SqlSession sqlSession;
private final Class<T> mapperInterface;
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 1. 获取MappedStatement的id(接口全类名.方法名)
String statementId = mapperInterface.getName() + "." + method.getName();
// 2. 根据返回值类型选择调用方法
if (返回集合) {
return sqlSession.selectList(statementId, args);
} else if (返回单个对象) {
return sqlSession.selectOne(statementId, args);
} else if (是增删改) {
return sqlSession.update(statementId, args);
}
}
}
// MapperProxyFactory - 创建代理对象
public class MapperProxyFactory<T> {
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface);
// 使用JDK动态代理创建代理对象
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy
);
}
}📝 面试回答要点
标准回答:
MyBatis 使用 JDK 动态代理 为 Mapper 接口生成代理对象。
- 启动时解析 Mapper.xml,将 SQL 语句封装成
MappedStatement注册到Configuration- 同时为每个 Mapper 接口创建
MapperProxyFactory- 调用
getMapper()时,通过MapperProxyFactory创建MapperProxy代理对象- 调用 Mapper 方法时,
MapperProxy.invoke()拦截调用- 根据方法名拼接 statementId,委托给
SqlSession执行
四、#{}和${}的区别
🔥 面试题:#{} 和 ${} 的区别是什么?
处理流程对比
对比表格
| 特性 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译占位符 | 字符串拼接 |
| 底层实现 | PreparedStatement | Statement |
| SQL注入 | ✅ 安全 | ❌ 有风险 |
| 性能 | 可复用预编译 | 每次重新编译 |
| 使用场景 | 参数值 | 表名、列名、排序 |
源码分析(伪代码)
// #{} 的处理 - ParameterHandler
public void setParameters(PreparedStatement ps) {
// 获取参数映射
List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping pm = parameterMappings.get(i);
Object value = 获取参数值(pm.getProperty());
// 使用TypeHandler设置参数,防止SQL注入
TypeHandler typeHandler = pm.getTypeHandler();
typeHandler.setParameter(ps, i + 1, value, pm.getJdbcType());
}
}
// ${} 的处理 - 在SQL解析阶段直接替换
public String parseDynamicSql(String sql, Object params) {
// 直接字符串替换,有SQL注入风险!
return sql.replace("${id}", String.valueOf(params.get("id")));
}📝 面试回答要点
标准回答:
#{}是预编译处理,会将参数替换为?,使用PreparedStatement设置参数,防止 SQL 注入${}是字符串拼接,直接将参数值拼接到 SQL 中,有 SQL 注入风险#{}适用于参数值,${}适用于动态表名、列名、排序字段等
五、缓存机制详解
🔥 面试题:MyBatis 的一级缓存和二级缓存有什么区别?
缓存架构
缓存查询流程
对比表格
| 特性 | 一级缓存 | 二级缓存 |
|---|---|---|
| 作用范围 | SqlSession 级别 | Mapper(namespace) 级别 |
| 默认状态 | 默认开启 | 默认关闭 |
| 生命周期 | SqlSession 创建到关闭 | 应用运行期间 |
| 数据共享 | 不能跨 SqlSession | 可跨 SqlSession |
| 失效条件 | 增删改、手动清空、不同查询 | 增删改 |
| 序列化 | 不需要 | 需要实现 Serializable |
源码分析(伪代码)
// 一级缓存 - BaseExecutor
public abstract class BaseExecutor implements Executor {
// 一级缓存,使用PerpetualCache(HashMap实现)
protected PerpetualCache localCache = new PerpetualCache("LocalCache");
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler) {
// 1. 生成缓存Key
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 2. 查询一级缓存
List<E> list = localCache.getObject(key);
if (list != null) {
return list; // 缓存命中
}
// 3. 查询数据库
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
// 4. 写入一级缓存
localCache.putObject(key, list);
return list;
}
}
// 二级缓存 - CachingExecutor(装饰器模式)
public class CachingExecutor implements Executor {
private final Executor delegate; // 被装饰的Executor
private final TransactionalCacheManager tcm = new TransactionalCacheManager();
public <E> List<E> query(MappedStatement ms, Object parameter,
RowBounds rowBounds, ResultHandler resultHandler) {
Cache cache = ms.getCache(); // 获取二级缓存
if (cache != null) {
// 1. 查询二级缓存
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
List<E> list = (List<E>) tcm.getObject(cache, key);
if (list != null) {
return list; // 二级缓存命中
}
}
// 2. 委托给BaseExecutor查询(会查一级缓存和数据库)
return delegate.query(ms, parameter, rowBounds, resultHandler);
}
}📝 面试回答要点
标准回答:
一级缓存:
- SqlSession 级别,默认开启
- 同一个 SqlSession 中相同查询会命中缓存
- 执行增删改、调用
clearCache()、不同查询条件会使缓存失效二级缓存:
- Mapper(namespace) 级别,默认关闭
- 需要在 Mapper.xml 中配置
<cache/>,实体类实现Serializable- SqlSession 关闭后数据才会写入二级缓存
- 可跨 SqlSession 共享,适合读多写少的场景
查询顺序:二级缓存 → 一级缓存 → 数据库
六、延迟加载原理
🔥 面试题:MyBatis 延迟加载的原理是什么?
延迟加载流程
源码分析(伪代码)
// 延迟加载代理 - 使用CGLIB或Javassist
public class LazyLoader implements MethodInterceptor {
private final ResultLoaderMap lazyLoadMap; // 延迟加载映射
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable {
String methodName = method.getName();
// 判断是否是getter方法且需要延迟加载
if (lazyLoadMap.hasLoader(getPropertyName(methodName))) {
// 触发延迟加载
lazyLoadMap.load(getPropertyName(methodName));
}
// 调用原方法
return proxy.invokeSuper(obj, args);
}
}
// ResultLoaderMap - 管理延迟加载
public class ResultLoaderMap {
private final Map<String, LoadPair> loaderMap = new HashMap<>();
public void load(String property) {
LoadPair pair = loaderMap.remove(property);
if (pair != null) {
// 执行SQL查询
pair.load();
}
}
}📝 面试回答要点
标准回答:
MyBatis 延迟加载使用 CGLIB 或 Javassist 创建代理对象。
- 查询主对象时,关联对象不立即加载,而是创建代理对象
- 代理对象中保存了加载关联对象所需的 SQL 和参数
- 当访问关联对象的属性时,代理对象拦截方法调用
- 触发延迟加载,执行 SQL 查询关联对象
- 将查询结果设置到主对象中
七、插件机制
🔥 面试题:MyBatis 插件的原理是什么?可以拦截哪些对象?
插件拦截点
插件原理
自定义插件示例
// 分页插件示例
@Intercepts({
@Signature(
type = StatementHandler.class,
method = "prepare",
args = {Connection.class, Integer.class}
)
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取StatementHandler
StatementHandler handler = (StatementHandler) invocation.getTarget();
// 2. 获取BoundSql
BoundSql boundSql = handler.getBoundSql();
String originalSql = boundSql.getSql();
// 3. 判断是否需要分页
Object paramObj = boundSql.getParameterObject();
if (paramObj instanceof PageParam) {
PageParam page = (PageParam) paramObj;
// 4. 改写SQL添加LIMIT
String pageSql = originalSql + " LIMIT " + page.getOffset() + "," + page.getPageSize();
// 5. 通过反射修改SQL
ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
}
// 6. 执行原方法
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// 使用Plugin.wrap创建代理
return Plugin.wrap(target, this);
}
}📝 面试回答要点
标准回答:
MyBatis 插件使用 JDK 动态代理 和 责任链模式 实现。可拦截的四大对象:
Executor:执行器,拦截增删改查StatementHandler:SQL 语句处理器ParameterHandler:参数处理器ResultSetHandler:结果集处理器原理:
- 通过
@Intercepts注解声明拦截点Plugin.wrap()判断是否需要代理,需要则创建 JDK 代理- 调用方法时,代理对象判断是否被拦截
- 被拦截则执行
Interceptor.intercept(),否则执行原方法
八、常见面试题汇总
基础题
| 问题 | 要点 |
|---|---|
| MyBatis 和 Hibernate 的区别? | 半自动 vs 全自动、SQL 控制、学习成本 |
| #{} 和 ${} 的区别? | 预编译 vs 字符串拼接、SQL 注入 |
| Mapper 接口如何与 XML 绑定? | namespace = 接口全类名,id = 方法名 |
| resultType 和 resultMap 的区别? | 自动映射 vs 自定义映射 |
进阶题
| 问题 | 要点 |
|---|---|
| MyBatis 执行流程? | SqlSession → Executor → StatementHandler → 数据库 |
| Mapper 接口没有实现类如何执行? | JDK 动态代理、MapperProxy |
| 一级缓存和二级缓存的区别? | SqlSession 级别 vs Mapper 级别 |
| 延迟加载原理? | CGLIB 代理、拦截 getter 方法 |
高级题
| 问题 | 要点 |
|---|---|
| 插件原理? | JDK 代理、责任链、四大对象 |
| 如何自定义 TypeHandler? | 继承 BaseTypeHandler、@MappedTypes |
| 如何防止 SQL 注入? | 使用 #{}、参数校验 |
| MyBatis 如何实现分页? | RowBounds(内存分页)、插件(物理分页) |
九、总结
掌握 MyBatis 需要:
- 会用:熟练使用各种 CRUD、动态 SQL、缓存配置
- 懂原理:理解代理机制、执行流程、缓存架构
- 能扩展:会写自定义插件、TypeHandler
- 善表达:面试时能清晰阐述原理,画图辅助说明
