一、从JDBC的痛点说起
回顾我们之前学的JDBC,写一个简单的查询需要多少行代码?
public User getUserById(Long id) throws SQLException {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/mydb", "root", "root");
String sql = "SELECT * FROM user WHERE id = ?";
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if (rs.next()) {
User user = new User();
user.setId(rs.getLong("id"));
user.setName(rs.getString("name"));
user.setAge(rs.getInt("age"));
return user;
}
return null;
} finally {
if (rs != null) rs.close();
if (pstmt != null) pstmt.close();
if (conn != null) conn.close();
}
}
看着这一大段代码,你会发现一个问题:真正和业务相关的只有三行——SQL语句、设置参数、处理结果。其他全是模板代码:获取连接、关闭连接、处理异常、释放资源。
这只是一个最简单的查询,如果项目里有100张表,每张表都要写这样的代码,那得重复多少次?而且每次都要小心翼翼地记得在finally里关闭资源,一旦忘记,数据库连接就被占着不释放,系统迟早崩溃。
于是,MyBatis诞生了。它的核心理念很简单:把那些重复的、容易出错的模板代码都封装起来,让开发者只关心SQL和结果映射。
二、JDBC vs MyBatis:一张表看懂差距
| 连接管理 | 手动获取、手动关闭 | SqlSession自动管理 |
| 参数设置 | 手动setXXX() | #{}占位符自动绑定 |
| 结果映射 | 手动遍历ResultSet赋值 | 自动映射到对象 |
| 资源释放 | finally里手动close | try-with-resources自动关闭 |
| SQL位置 | 混在Java代码里 | 分离到XML或注解 |
| 动态SQL | 手动拼接字符串 | 标签支持(if/where/foreach) |
三、MyBatis的核心组件
在深入执行流程之前,先认识几个关键组件:
-
SqlSessionFactory:整个应用的“心脏”,负责创建SqlSession。一个应用只需要一个实例(单例)。
-
SqlSession:相当于JDBC的Connection,代表一次数据库会话。它不是线程安全的,用完必须关闭。
-
Executor:执行器,真正干活的。负责SQL执行、缓存管理、事务控制。
-
MappedStatement:封装了XML里配置的SQL语句、参数类型、返回类型等信息。
四、MyBatis执行流程
我们用一个最简单的查询来跟踪MyBatis的执行流程:
4.1 第一步:读取配置文件
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
mybatis-config.xml里配置了数据库连接信息、事务管理器、以及Mapper映射文件的位置。 如果对于MyBatis配置有疑问可以参考我之前的文章(https://mp.csdn.net/mp_blog/creation/editor/157480246)
4.2 第二步:创建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
这一步内部做了很多事情:
-
解析mybatis-config.xml里的所有配置
-
解析environments(数据库环境)、settings(全局设置)、mappers(映射器)等标签
-
把解析结果封装成一个Configuration对象(全局配置)
-
用这个Configuration对象创建DefaultSqlSessionFactory
相当于JDBC里加载驱动、准备连接信息的那一步,但MyBatis把它做成了“一次解析,永久使用”。
4.3 第三步:打开SqlSession(获取连接)
SqlSession sqlSession = sqlSessionFactory.openSession();
这一步是关键。看一下openSession()内部做了什么:
-
从Configuration里获取配置好的Executor类型(默认是SimpleExecutor)
-
从Configuration里获取Environment,再从Environment里获取DataSource
-
从DataSource获取数据库连接(如果开启了事务,还会设置autoCommit=false)
-
创建一个Executor(执行器),把这个连接包装进去
-
用这个Executor创建DefaultSqlSession
对应DriverManager.getConnection()——获取数据库连接。
4.4 第四步:获取Mapper接口(动态代理)
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
这一步是MyBatis的精髓。UserMapper是一个接口,我们没有写实现类,那这个对象是怎么来的?答案是动态代理。
getMapper()的调用链:
-
DefaultSqlSession → Configuration → MapperRegistry
-
MapperRegistry里有个knownMappers的Map,存着所有Mapper接口的代理工厂
-
根据接口类型取出对应的MapperProxyFactory
-
代理工厂创建MapperProxy(实现了InvocationHandler)
-
返回代理对象
当我们调用userMapper.getById(1L)时,实际调用的是MapperProxy的invoke方法。
这一步在JDBC里没有直接对应,它是MyBatis为了让我们不用写实现类而做的封装。
4.5 第五步:执行SQL
User user = userMapper.getById(1L);
看看这一行背后发生了什么:
MapperProxy.invoke():
-
根据调用的方法,创建MapperMethod对象(如果缓存里没有的话)
-
MapperMethod里有两个内部类:SqlCommand(记录SQL的id和类型)和MethodSignature(记录方法信息)
-
调用MapperMethod.execute()
MapperMethod.execute():
-
根据SQL类型决定调用SqlSession的哪个方法
-
这里调用的是sqlSession.selectOne()
DefaultSqlSession.selectOne():
-
根据statementId(比如"UserMapper.getById")从Configuration里取出MappedStatement
-
把查询交给Executor去执行
Executor.query()(以SimpleExecutor为例):
-
检查一级缓存(Local Cache)里有没有
-
如果没有,调用doQuery()去数据库查
-
创建StatementHandler
-
StatementHandler里包含ParameterHandler(处理参数)和ResultSetHandler(处理结果)
-
通过ParameterHandler设置参数(对应JDBC的pstmt.setXXX())
-
执行JDBC的PreparedStatement.executeQuery()
-
通过ResultSetHandler处理ResultSet,把结果映射成对象(对应JDBC的rs.getXXX()和user.setXXX())
对应pstmt.executeQuery() + rs.next() + 手动赋值。
4.6 第六步:提交事务(如果需要)
如果执行的是修改操作(insert/update/delete),或者你想手动控制事务:
sqlSession.commit(); // 提交事务
MyBatis的事务默认是手动提交模式(autoCommit=false)。也就是说,执行完增删改后,必须显式调用commit(),否则数据不会真正写入数据库。
commit()内部做了什么:
-
调用Executor的commit()方法
-
Executor拿到它内部包装的Transaction对象
-
Transaction对象拿到真正的JDBC Connection
-
调用connection.commit()
4.7 第七步:回滚事务
try {
// 执行操作
sqlSession.commit();
} catch (Exception e) {
sqlSession.rollback(); // 回滚事务
throw e;
}
rollback()的内部流程:
-
调用Executor的rollback()方法
-
Executor拿到Transaction对象
-
Transaction拿到JDBC Connection
-
调用connection.rollback()
4.8 第八步:关闭SqlSession(释放资源)
sqlSession.close();
对应finally块里那一堆rs.close()、pstmt.close()、conn.close()。
五、总结
回过头看,MyBatis并没有发明新的东西,它只是把JDBC里那些重复的、繁琐的、容易出错的操作都封装起来了:
-
连接管理 → SqlSessionFactory和SqlSession
-
参数设置 → #{}占位符
-
结果映射 → 反射自动赋值
-
资源释放 → try-with-resources自动close
-
事务控制 → commit()和rollback()方法
可能会有疑问:用MyBatis是不是比直接写JDBC慢?确实,多了一层封装,理论上会有一点点性能损耗。但这个损耗跟开发效率、代码可维护性比起来,完全可以接受。再说了,MyBatis内部最终还是调用JDBC,那点损耗微乎其微。
最重要的是,用MyBatis之后,我们可以把精力集中在业务逻辑和SQL优化上,而不是跟那些模板代码死磕。
以上是我对MyBatis执行流程的学习理解,如果有不准确的地方,欢迎指正。


