MyBatis 作为 Java 持久层框架的 "顶流",其 "接口无实现却能执行 SQL" 的设计,是 "动态代理 + 配置驱动" 的经典实践。但多数开发者仅停留在 "会用" 层面,对其底层执行逻辑一知半解。本文从源码跟踪、原理推导、实战验证 三个维度,完整拆解 MyBatis 从Mapper接口调用到数据库返回结果的全流程,同时解析流程中的核心设计思想与性能优化点。
一、前置知识:MyBatis 的核心设计思想
在分析流程前,先明确 MyBatis 的两个核心设计思想(这是理解流程的基础):
- 配置驱动:将 SQL、参数映射、结果映射等逻辑从 Java 代码中剥离,通过 XML / 注解配置管理,降低业务代码与持久层的耦合;
- 动态代理 :通过 JDK 动态代理为
Mapper接口生成代理类,拦截接口方法调用,将请求转发给 MyBatis 的核心执行组件。
二、核心组件深度解析:执行流程的 "执行者"
MyBatis 的执行流程由多个核心组件协作完成,每个组件的职责与设计细节如下:
|-------------------|----------------------------------------------|-----------------------------------------------------------------------------|
| 组件名 | 核心职责 | 设计细节 |
| Configuration | 全局配置中心,存储所有配置信息 | 单例对象,包含MappedStatement、数据源、插件、类型处理器等,是 MyBatis 的 "大脑" |
| SqlSessionFactory | 创建SqlSession的工厂 | 基于Configuration构建,单例存在,项目启动时初始化一次 |
| SqlSession | 数据库会话,封装执行器与事务 | 非线程安全,代表一次数据库连接,业务代码通过它操作数据库 |
| Executor | SQL 执行器,负责 SQL 的调度、缓存、批处理 | 有三种实现:SimpleExecutor(默认,简单执行)、BatchExecutor(批处理)、CachingExecutor(带缓存) |
| MappedStatement | 封装单个 SQL 的所有信息(SQL 语句、参数映射、结果映射、执行方式等) | 对应Mapper.xml中的一个 SQL 标签(如<select>),ID 为 "接口全限定名 + 方法名" |
| ParameterHandler | 解析接口参数,绑定到 SQL 占位符 | 默认实现DefaultParameterHandler,支持#{}(预编译)和${}(字符串拼接) |
| ResultSetHandler | 将数据库返回的ResultSet映射为 Java 对象 | 默认实现DefaultResultSetHandler,支持简单对象、集合、嵌套结果映射 |
| StatementHandler | 操作 JDBC 的Statement/PreparedStatement执行 SQL | 负责 SQL 的预编译、参数设置、执行触发,是 MyBatis 与 JDBC 的 "桥梁" |
三、执行全流程:从Mapper接口到数据库返回(源码 + 实战)
以 "根据用户 ID 查询用户信息" 为例,完整流程分为初始化阶段 和执行阶段,以下是每个步骤的源码跟踪与实战验证。
(一)初始化阶段:加载配置,构建核心对象
MyBatis 启动时,会完成 "配置解析→核心对象初始化" 的工作,这是后续执行的基础。
步骤 1:解析mybatis-config.xml,构建Configuration
MyBatis 通过XmlConfigBuilder解析全局配置文件,将数据源、映射器等信息存入Configuration。
源码跟踪:
java
// 读取配置文件输入流
InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml");
// 创建XmlConfigBuilder,解析配置
XmlConfigBuilder parser = new XmlConfigBuilder(inputStream, null, null);
// 解析后生成Configuration
Configuration configuration = parser.parse();
parser.parse()的核心逻辑是解析<environments>(数据源、事务)、<mappers>(映射器)等标签,将信息存入Configuration。
步骤 2:解析Mapper.xml,注册MappedStatement
XmlConfigBuilder会调用XmlMapperBuilder解析Mapper.xml,将每个 SQL 标签封装为MappedStatement并注册到Configuration。
实战验证 :假设UserMapper.xml中有如下 SQL:
XML
<mapper namespace="com.example.mapper.UserMapper">
<select id="getById" parameterType="Long" resultType="com.example.entity.User">
SELECT id, name, age FROM user WHERE id = #{id}
</select>
</mapper>
XmlMapperBuilder解析该标签时,会生成一个MappedStatement,其id为com.example.mapper.UserMapper.getById,并将其添加到Configuration的mappedStatements(一个Map)中。
源码片段:
java
// XmlMapperBuilder解析<select>标签
private void processSelectElement(XNode node) {
// 获取SQL标签的id、parameterType、resultType等属性
String id = node.getStringAttribute("id");
String parameterType = node.getStringAttribute("parameterType");
String resultType = node.getStringAttribute("resultType");
// 构建MappedStatement
MappedStatement ms = buildMappedStatement(id, parameterType, resultType, node);
// 注册到Configuration
configuration.addMappedStatement(ms);
}
步骤 3:构建SqlSessionFactory
通过SqlSessionFactoryBuilder将Configuration封装为SqlSessionFactory(单例对象)。
实战代码:
java
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(configuration);
SqlSessionFactory是 MyBatis 的 "入口",后续所有数据库操作都通过它创建的SqlSession完成。
(二)执行阶段:从Mapper接口调用到结果返回
当业务代码调用userMapper.getById(1L)时,MyBatis 的执行流程正式启动,以下是每个步骤的细节:
步骤 1:创建SqlSession
从SqlSessionFactory中获取SqlSession,它代表一次数据库连接(非线程安全,需在方法内创建并关闭)。
实战代码:
java
// openSession(true)表示自动提交事务,默认false(手动提交)
SqlSession sqlSession = sqlSessionFactory.openSession(true);
源码逻辑:
SqlSessionFactory.openSession()会创建DefaultSqlSession,同时初始化Executor和Transaction:
java
// DefaultSqlSessionFactory的openSession方法
@Override
public SqlSession openSession(boolean autoCommit) {
// 获取环境配置(数据源、事务管理器)
Environment environment = configuration.getEnvironment();
// 创建事务
Transaction tx = transactionFactory.newTransaction(environment.getDataSource(), autoCommit);
// 创建Executor(默认是SimpleExecutor)
Executor executor = configuration.newExecutor(tx);
// 创建DefaultSqlSession
return new DefaultSqlSession(configuration, executor, autoCommit);
}
步骤 2:生成Mapper代理对象
通过SqlSession.getMapper()获取Mapper接口的代理对象,这是 MyBatis "接口无实现却能执行 SQL" 的核心。
实战代码:
java
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
源码跟踪:
SqlSession.getMapper()会调用MapperRegistry.getMapper(),通过 JDK 动态代理生成代理类:
java
// MapperRegistry的getMapper方法
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// 获取Mapper代理工厂
MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
// 生成代理对象
return mapperProxyFactory.newInstance(sqlSession);
}
// MapperProxyFactory的newInstance方法
public T newInstance(SqlSession sqlSession) {
// 创建InvocationHandler(代理逻辑的实现)
MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
// 生成JDK动态代理对象
return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[]{mapperInterface}, mapperProxy);
}
最终生成的代理对象是MapperProxy(实现了InvocationHandler),它会拦截所有Mapper接口的方法调用。
步骤 3:拦截接口方法调用,转发请求
当调用userMapper.getById(1L)时,代理对象的MapperProxy.invoke()方法会被触发,将请求转发给SqlSession。
源码片段:
java
// MapperProxy的invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 处理Object类的方法(如toString())
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 处理接口方法,转发给SqlSession
return cachedMapperMethod(method).execute(sqlSession, args);
}
// MapperMethod的execute方法
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
// 根据SQL类型(SELECT/INSERT/UPDATE/DELETE)调用SqlSession的对应方法
switch (command.getType()) {
case SELECT:
// 处理查询,调用sqlSession.selectOne()
result = sqlSession.selectOne(command.getName(), param);
break;
// 省略其他类型的处理...
}
return result;
}
步骤 4:匹配MappedStatement,执行 SQL
SqlSession.selectOne()会从Configuration中找到对应的MappedStatement,然后交给Executor执行。
源码跟踪:
java
// DefaultSqlSession的selectOne方法
@Override
public <T> T selectOne(String statement, Object parameter) {
// 调用selectList,取第一个结果
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
}
// 省略异常处理...
}
// DefaultSqlSession的selectList方法
@Override
public <E> List<E> selectList(String statement, Object parameter) {
// 获取MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 交给Executor执行
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
}
步骤 5:Executor调度执行,完成参数与结果映射
Executor.query()会调用StatementHandler处理 SQL 预编译,ParameterHandler处理参数绑定,ResultSetHandler处理结果映射。核心流程:
ParameterHandler解析参数 :将接口入参1L绑定到 SQL 的#{id}占位符,生成PreparedStatement;StatementHandler执行 SQL :调用PreparedStatement.execute(),发送 SQL 到数据库;ResultSetHandler映射结果 :将数据库返回的ResultSet转换为User对象。
步骤 6:关闭SqlSession,释放资源
执行完成后,必须关闭SqlSession(建议在finally块中执行),释放数据库连接等资源。
实战代码:
java
finally {
if (sqlSession != null) {
sqlSession.close();
}
}
四、流程中的设计亮点与性能优化
1.Executor的缓存设计 :CachingExecutor会缓存查询结果(二级缓存),避免重复查询数据库;可通过@CacheNamespace开启。
2.BatchExecutor的批处理优化 :对于批量插入 / 更新操作,使用ExecutorType.BATCH创建SqlSession,可减少数据库连接次数,提升性能:
java
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
3.参数映射的预编译安全 :ParameterHandler对#{id}的处理是预编译(PreparedStatement),避免 SQL 注入;而${id}是字符串拼接,需谨慎使用。
五、实战验证:跟踪执行流程
通过 IDEA 的断点调试,可清晰看到流程的执行顺序:
- 在
MapperProxy.invoke()处打断点,观察接口方法被拦截; - 在
DefaultSqlSession.selectOne()处打断点,观察MappedStatement的匹配; - 在
Executor.query()处打断点,观察 SQL 的执行与结果映射。
六、总结
MyBatis 的执行流程是 "配置解析→代理生成→请求转发→组件协作→结果返回 " 的完整链路,其核心是通过 "动态代理" 屏蔽了Mapper接口的实现细节,通过 "配置驱动" 分离了 SQL 与业务代码。理解这一流程,不仅能解决日常开发中的持久层问题,更能深入体会 MyBatis 的设计思想,为自定义持久层框架或性能优化提供思路。