一句话总结
MyBatis 核心执行流程:读取配置文件 → SqlSessionFactoryBuilder.build() → 创建 SqlSessionFactory → openSession() 获取 SqlSession → getMapper() 获取 Mapper 代理对象(JDK 动态代理)→ MapperProxy.invoke() → MapperMethod.execute() → 根据 SQL 类型(SELECT/INSERT/UPDATE/DELETE)调用 SqlSession 对应方法 → Executor 执行器处理(二级缓存 → 一级缓存 → 数据库查询)→ StatementHandler 创建 Statement → ParameterHandler 设置参数 → 执行 SQL → ResultSetHandler 处理结果集 → 返回映射结果。核心是 动态代理 + 插件拦截链 + 四级执行器。
初级理解
MyBatis 使用流程
// 1. 读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
// 2. 构建 SqlSessionFactory
SqlSessionFactory sqlSessionFactory =
new SqlSessionFactoryBuilder().build(inputStream);
// 3. 获取 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {
// 4. 方式一:直接调用(statement 方式)
User user = session.selectOne(
"com.example.mapper.UserMapper.selectById", 1);
// 5. 方式二:Mapper 接口代理(推荐)
UserMapper mapper = session.getMapper(UserMapper.class);
User user2 = mapper.selectById(1);
}
#{} 和 ${} 的区别
| 对比维度 | #{} | ${} |
| 处理方式 | 预编译占位符 ? | 直接字符串拼接 |
| SQL 注入 | 安全,防注入 | 不安全,可能被注入 |
| 参数替换 | 自动加引号 | 不自动加引号 |
| 使用场景 | WHERE 条件值 | 表名、列名、ORDER BY |
| 底层实现 | PreparedStatement | Statement |
中级深入
核心组件架构
MyBatis 核心组件调用链:
SqlSessionFactoryBuilder
↓ build()
SqlSessionFactory (DefaultSqlSessionFactory)
↓ openSession()
SqlSession (DefaultSqlSession)
↓ getMapper() / selectOne()
Executor (执行器)
↓ query()
StatementHandler (语句处理器)
↓ parameterize() → query()
ParameterHandler (参数处理器) → PreparedStatement → ResultSetHandler (结果处理器)
# 四大核心对象:
# 1. Executor:执行器,负责SQL执行(增删改查)
# 2. StatementHandler:语句处理器,创建Statement
# 3. ParameterHandler:参数处理器,设置预编译参数
# 4. ResultSetHandler:结果处理器,映射ResultSet到Java对象
Executor 执行器体系
# Executor 继承体系
Executor (接口)
├── BaseExecutor (抽象类,模板方法模式)
│ ├── SimpleExecutor (默认,每次创建新Statement)
│ ├── ReuseExecutor (复用Statement)
│ └── BatchExecutor (批量执行)
└── CachingExecutor (装饰器模式,二级缓存)
# BaseExecutor.query() 核心流程:
public <E> List<E> query(MappedStatement ms, Object param,
RowBounds rowBounds, ResultHandler handler) {
BoundSql boundSql = ms.getBoundSql(param);
CacheKey key = createCacheKey(ms, param, rowBounds, boundSql);
return query(ms, param, rowBounds, handler, key, boundSql);
}
# 一级缓存查询流程:
# 1. 根据 CacheKey 查一级缓存(localCache)
# 2. 命中 → 直接返回
# 3. 未命中 → queryFromDatabase() → 放入一级缓存 → 返回
Mapper 代理原理
# getMapper() 本质是 JDK 动态代理
public <T> T getMapper(Class<T> type) {
return configuration.getMapper(type, this);
}
# MapperProxyFactory.newInstance()
public T newInstance(SqlSession sqlSession) {
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession,
mapperInterface, methodCache);
return (T) Proxy.newProxyInstance(
mapperInterface.getClassLoader(),
new Class[]{mapperInterface},
mapperProxy
);
}
# MapperProxy.invoke() 核心逻辑:
# 1. 判断方法是否是 Object 类方法(toString/hashCode/equals)
# 2. 判断是否是 default 方法(Java 8+)
# 3. 创建 MapperMethod 并执行 execute()
# 4. MapperMethod 根据 SQL 类型分发到 SqlSession 对应方法
高级进阶
插件拦截机制
# MyBatis 插件基于责任链模式 + JDK 动态代理
# 可拦截的四大对象:
# 1. Executor (update/query/flushStatements/commit/rollback)
# 2. StatementHandler (prepare/parameterize/batch/update/query)
# 3. ParameterHandler (getParameterObject/setParameters)
# 4. ResultSetHandler (handleResultSets/handleOutputParameters)
# 插件原理:
# 1. 解析 <plugins> 配置,反射创建 Interceptor 实例
# 2. InterceptorChain.pluginAll(target) 层层代理
# 3. 每个插件返回 target 的代理对象
# 4. 最终 Executor/StatementHandler 等被多层代理包裹
# 示例:PageHelper 分页插件拦截 Executor.query()
@Intercepts({
@Signature(type = Executor.class, method = "query",
args = {MappedStatement.class, Object.class,
RowBounds.class, ResultHandler.class})
})
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
// 1. 获取参数中的分页信息
// 2. 查询总记录数(SELECT COUNT(*))
// 3. 修改原 SQL 添加 LIMIT 子句
// 4. 执行分页查询
// 5. 封装 PageInfo 返回
return invocation.proceed();
}
}
MyBatis 初始化流程
# SqlSessionFactoryBuilder.build() 核心流程:
XMLConfigBuilder.parse()
↓
parseConfiguration(XNode root)
↓ 依次解析:
1. properties(属性配置)
2. settings(全局设置,如驼峰命名 mapUnderscoreToCamelCase)
3. typeAliases(类型别名)
4. plugins(插件注册到 InterceptorChain)
5. objectFactory / objectWrapperFactory
6. environments(数据源 + 事务管理器)
7. mappers(Mapper 映射文件/接口注册)
↓
Configuration 对象构建完成
↓
new DefaultSqlSessionFactory(configuration)
# Mapper 注册流程:
# XML 方式:解析 <mapper resource="..."> → XMLMapperBuilder
# 接口方式:<mapper class="..."> → MapperAnnotationBuilder
# 包扫描:<package name="..."> → 扫描所有接口
# 每个 MappedStatement 包含:
# - id(namespace + statementId)
# - sqlSource(动态SQL/静态SQL)
# - sqlCommandType(SELECT/INSERT/UPDATE/DELETE)
# - resultMaps / parameterMap
一级缓存和二级缓存
# 一级缓存(SqlSession 级别,默认开启)
# 实现:BaseExecutor.localCache (PerpetualCache,本质是 HashMap)
# 生命周期:与 SqlSession 绑定,SqlSession 关闭即清空
# 失效条件:
# 1. 不同 SqlSession
# 2. 同一 SqlSession 执行了 INSERT/UPDATE/DELETE(清空缓存)
# 3. 手动 clearCache()
# 4. 查询条件不同(CacheKey 不同)
# 二级缓存(Mapper 级别,需手动开启)
# 实现:CachingExecutor → TransactionalCacheManager
# 开启方式:
# 1. mybatis-config.xml: <setting name="cacheEnabled" value="true"/>
# 2. Mapper.xml: <cache/> 标签
# 3. 实体类实现 Serializable
# 二级缓存执行流程:
# CachingExecutor.query()
# → 查二级缓存(TransactionalCache)
# → 未命中 → BaseExecutor.query()
# → 查一级缓存(localCache)
# → 未命中 → queryFromDatabase()
# → 放入二级缓存(需事务提交后)
# 注意事项:
# 1. 二级缓存是 namespace 级别的
# 2. 多表关联查询容易出现脏数据
# 3. 生产环境谨慎使用,建议用 Redis 等分布式缓存替代
实战场景
# 场景1:MyBatis + Spring Boot 整合
# application.yml
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
configuration:
map-underscore-to-camel-case: true # 驼峰映射
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 场景2:批量插入优化
# 方式一:foreach 拼接(小数据量)
<insert id="batchInsert">
INSERT INTO user(name, age) VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.age})
</foreach>
</insert>
# 方式二:BatchExecutor(大数据量,推荐)
try (SqlSession session = sqlSessionFactory.openSession(
ExecutorType.BATCH)) {
UserMapper mapper = session.getMapper(UserMapper.class);
for (User user : list) {
mapper.insert(user);
}
session.commit(); // 一次性提交
}
# 场景3:#{} vs ${} 安全使用
# 正确:WHERE 条件用 #{}
SELECT * FROM user WHERE id = #{id}
# 正确:动态表名/排序用 ${}(需白名单校验)
SELECT * FROM ${tableName} ORDER BY ${orderColumn} ${sortDirection}
# 使用前必须校验:
private static final Set<String> ALLOWED_TABLES = Set.of("user", "order");
private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "create_time");
private static final Set<String> ALLOWED_DIRECTIONS = Set.of("ASC", "DESC");
面试模拟
面试官:MyBatis 的执行流程是怎样的?
你:首先通过 SqlSessionFactoryBuilder 读取配置文件构建 SqlSessionFactory,然后 openSession() 获取 SqlSession。通过 getMapper() 获取 Mapper 接口的 JDK 动态代理对象。调用方法时,MapperProxy 拦截并创建 MapperMethod,根据 SQL 类型(SELECT/INSERT/UPDATE/DELETE)调用 SqlSession 对应方法,最终由 Executor 执行。Executor 先查二级缓存,再查一级缓存,未命中则通过 StatementHandler 创建 Statement、ParameterHandler 设置参数、执行 SQL、ResultSetHandler 处理结果集。
面试官:#{} 和 ${} 的区别是什么?
你:#{} 是预编译占位符,底层用 PreparedStatement,安全防注入,自动加引号,用于 WHERE 条件值。${} 是字符串拼接,底层用 Statement,不安全,用于动态表名/列名/ORDER BY,使用时必须做白名单校验。