MyBatis 核心执行流程?

2025年 阅读约 15 分钟 面试指南 · MyBatis

深入解析MyBatis核心执行流程:SqlSessionFactory构建过程、SqlSession执行原理、Executor执行器体系、StatementHandler与ResultSetHandler工作机制、#{}和${}的本质区别,附面试模拟问答。

一句话总结

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
底层实现PreparedStatementStatement

中级深入

核心组件架构

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,使用时必须做白名单校验。