AI摘要
在日常开发中,我们早已习惯了这样的写法:
User user = userMapper.selectById(1L);这行代码简洁得令人安心。但你是否曾停下来想过,这个没有任何实现的接口方法selectById,究竟是如何神奇地变成了一个SELECT * FROM user WHERE id = ?的数据库调用?
今天,我们就化身“源码侦探”,深入MyBatis的内部,完整地跟踪一条SQL语句的“一生”。这不仅是为了满足好奇心,更是为了在遇到那些“诡异”的问题时(比如为什么这里的参数没生效?为什么缓存莫名其妙?),我们能心中有数,直击要害。
序幕:一切的起点——SqlSessionFactory
故事开始于一个重量级对象:SqlSessionFactory。它通常在我们的代码中这样诞生:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);SqlSessionFactoryBuilder.build()方法是MyBatis初始化的核心。它会:
- 解析
mybatis-config.xml全局配置文件。 - 加载所有的
*Mapper.xml文件,并将每一个<select|insert|update|delete>标签解析成一个MappedStatement对象。 - 这个
MappedStatement是真正的核心,它包含了这条SQL语句的所有信息:SQL源码、参数类型、结果类型、缓存配置、执行类型(如SELECT)等。
可以把SqlSessionFactory想象成一个已经装配完毕的汽车工厂,而MappedStatement就是一辆辆已经设计好蓝图、等待生产的汽车图纸。
第一幕:获取舞台——SqlSession
有了工厂,我们就可以获取一次数据库操作的“舞台”——SqlSession。
// 通常我们通过这个方式获取Mapper接口的代理对象
UserMapper userMapper = sqlSession.getMapper(UserMapper.class);
// 或者直接使用SqlSession的方法(更底层,但不常用)
// User user = sqlSession.selectOne("com.example.mapper.UserMapper.selectById", 1L);getMapper()方法是魔法开始的地方。 这里用到了动态代理。MyBatis并没有为你UserMapper接口生成一个实体类,而是通过java.lang.reflect.Proxy创建了一个代理对象。
当你调用userMapper.selectById(1L)时,实际上调用的是Proxy的InvocationHandler的invoke方法。在MyBatis中,这个处理器是MapperProxy。
第二幕:代理的魔法——MapperProxy
MapperProxy.invoke()方法是整个执行流程的“交通枢纽”。它的核心逻辑如下:
// 简化版的 MapperProxy.invoke 逻辑
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 1. 如果是调用Object本身的方法(如toString, hashCode),直接执行
if (Object.class.equals(method.getDeclaringClass())) {
return method.invoke(this, args);
}
// 2. 如果是接口的默认方法(Java8+),也直接执行
if (method.isDefault()) {
return invokeDefaultMethod(proxy, method, args);
}
// 3. 核心:将方法调用转换为一个命令执行
// 这里会找到一个 MapperMethod 对象
MapperMethod mapperMethod = cachedMapperMethod(method);
// 然后执行 execute 方法
return mapperMethod.execute(sqlSession, args);
}MapperMethod是一个非常重要的内部类。它内部有两个关键属性:
SqlCommand: 包含了这条SQL语句对应的唯一ID(如"com.example.mapper.UserMapper.selectById")和命令类型(SELECT,INSERT等)。MethodSignature: 包含了方法签名的详细信息(返回类型、参数注解等)。
第三幕:决策与分发——MapperMethod.execute()
mapperMethod.execute(sqlSession, args)是决策中心。它会根据SQL命令的类型,决定调用SqlSession的哪个方法。
对于我们selectById这个查询方法,它会走进这个分支:
// 简化版的 MapperMethod.execute 逻辑
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case SELECT: {
// 判断方法返回类型
if (method.returnsVoid() && method.hasResultHandler()) {
// ... 处理有结果处理器的情况
} else if (method.returnsMany()) {
// ... 返回集合(List, Map等)
} else if (method.returnsMap()) {
// ... 返回Map
} else if (method.returnsCursor()) {
// ... 返回Cursor
} else {
// 我们的 selectById 方法返回单个对象,走进这个分支!
Object param = method.convertArgsToSqlCommandParam(args);
// 注意!这里调用了 sqlSession.selectOne
result = sqlSession.selectOne(command.getName(), param);
}
break;
}
// ... 处理 INSERT, UPDATE, DELETE 的情况
}
return result;
}看,经过层层转发,最终调用了sqlSession.selectOne(command.getName(), param)。其中,command.getName()就是我们在Mapper.xml中定义的namespace + id,例如"com.example.mapper.UserMapper.selectById"。
第四幕:进入核心——SqlSession的实现 DefaultSqlSession
SqlSession是一个接口,我们通常使用的实现类是DefaultSqlSession。现在,我们终于要进入执行SQL的真正腹地。
DefaultSqlSession.selectOne()方法非常简单:
public <T> T selectOne(String statement, Object parameter) {
// 调用了 selectList,然后取第一个元素
List<T> list = this.selectList(statement, parameter);
if (list.size() == 1) {
return list.get(0);
} else if (list.size() > 1) {
throw new TooManyResultsException("Expected one result (or null) to be returned, but found: " + list.size());
} else {
return null;
}
}所以,selectOne本质上是selectList的特例。真正的核心在selectList中:
public <E> List<E> selectList(String statement, Object parameter) {
return this.selectList(statement, parameter, RowBounds.DEFAULT);
}
public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
try {
// 1. 根据 statement id,从 Configuration 中获取我们之前解析好的 MappedStatement
MappedStatement ms = configuration.getMappedStatement(statement);
// 2. 调用执行器 Executor 来执行查询!
return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}注意看!SqlSession并没有亲自去执行SQL,它只是一个门面(Facade Pattern),真正干活的苦力是Executor(执行器)!
第五幕:真正的执行者——Executor与拦截器插件
Executor是MyBatis执行过程中的核心引擎。它定义了SQL执行、事务管理、缓存等核心操作。它有几种实现:
SimpleExecutor: 默认的,每次执行都会创建一个新的PreparedStatement。ReuseExecutor: 会复用PreparedStatement。BatchExecutor: 用于批处理。
在执行器开始工作之前,MyBatis提供了一个强大的扩展点:拦截器(Interceptor)。 如果你配置了分页插件(如PageHelper),就是在这里动的手脚。它会生成count查询,或修改原始SQL加入limit语句。
第六幕:缓存、语句与参数处理——BaseExecutor.query()
我们以最常用的SimpleExecutor的父类BaseExecutor为例,看看query方法做了什么:
// BaseExecutor.query 方法(简化版)
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
// 1. 根据传入的参数,动态地生成一条SQL语句(BoundSql)
BoundSql boundSql = ms.getBoundSql(parameter);
// 2. 创建一个缓存Key(这个Key由SQL语句、参数、分页信息等共同决定,保证唯一性)
CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
// 3. 继续查询
return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 检查一级缓存(本地缓存,SqlSession级别)
List<E> list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
if (list != null) {
// 缓存命中
handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
} else {
// 缓存未命中,从数据库查询
list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
}
// ... 处理存储过程输出参数等
return list;
}
private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
// 1. 在缓存中放置一个占位符,防止并发重复查询
localCache.putObject(key, EXECUTION_PLACEHOLDER);
try {
// 2. 执行 doQuery 方法!这里由子类(如SimpleExecutor)实现
List<E> list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
// 3. 将真实结果放入缓存
localCache.putObject(key, list);
return list;
} finally {
localCache.removeObject(EXECUTION_PLACEHOLDER);
}
}这里清晰地展示了一级缓存的逻辑:先查缓存,没有再查库。
第七幕:创建语句与参数设置——SimpleExecutor.doQuery()
现在,接力棒传到了SimpleExecutor.doQuery()手中:
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
Statement stmt = null;
try {
Configuration configuration = ms.getConfiguration();
// 1. 创建 StatementHandler(语句处理器)
// 它会根据MappedStatement的statementType(STATEMENT, PREPARED, CALLABLE)创建不同的处理器
StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
// 2. 准备语句:包括创建PreparedStatement、设置参数
stmt = prepareStatement(handler, ms.getStatementLog());
// 3. 执行查询,并由ResultSetHandler处理结果集
return handler.query(stmt, resultHandler);
} finally {
closeStatement(stmt);
}
}
private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException {
Connection connection = getConnection(statementLog);
// 调用 StatementHandler.prepare 创建 PreparedStatement
Statement stmt = handler.prepare(connection, transaction.getTimeout());
// 调用 StatementHandler.parameterize 设置参数!
handler.parameterize(stmt);
return stmt;
}StatementHandler.parameterize(stmt)是设置SQL参数的地方。 这里会用到另一个重要组件:ParameterHandler(参数处理器)。它负责将Java方法的参数(比如Long id = 1L)转换成JDBC所需的类型(比如ps.setLong(1, 1L))。
第八幕:最终执行与结果处理
最后,在PreparedStatementHandler.query()中,我们终于看到了熟悉的JDBC代码:
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
PreparedStatement ps = (PreparedStatement) statement;
// 1. 执行SQL!
ps.execute();
// 2. 使用 ResultSetHandler 处理结果集
return resultSetHandler.handleResultSets(ps);
}ResultSetHandler(结果集处理器)是最后一个关键角色。它根据MappedStatement中定义的resultType或resultMap,通过反射,将JDBC返回的ResultSet一行行地映射成我们需要的Java对象(比如User对象)。
全流程总结
让我们用一张简图来回顾这趟漫长的旅程:
Java代码: userMapper.selectById(1L)
↓ (动态代理)
MapperProxy.invoke()
↓
MapperMethod.execute()
↓
DefaultSqlSession.selectOne() -> selectList()
↓ (获取MappedStatement,委托给Executor)
Executor.query() // 处理一级/二级缓存
↓ (缓存未命中,真正执行)
BaseExecutor.queryFromDatabase()
↓
SimpleExecutor.doQuery()
↓ (创建三大处理器)
1. StatementHandler.prepare() // 创建PreparedStatement
2. ParameterHandler.setParameters() // 设置SQL参数 `ps.setLong(1, 1L)`
↓
PreparedStatement.execute() // 执行SQL
↓
3. ResultSetHandler.handleResultSets() // 将ResultSet映射为User对象
↓
最终返回: User user为什么这很重要?
理解这个过程,能让你在面对日常开发中的许多“灵异事件”时,不再束手无策。
- 参数映射错误? 你知道该去查
ParameterHandler和TypeHandler。 - 结果映射失败? 你的目标是
ResultSetHandler和对应的ResultMap配置。 - 一级缓存导致数据不一致? 你明白是因为同一个
SqlSession内的查询复用了缓存,知道该在合适的时候调用sqlSession.clearCache()。 - 插件(分页、日志)如何工作? 你清楚它们是在
Executor被调用前,通过动态代理插入了自己的逻辑。
源码阅读不是目的,而是手段。通过这次对MyBatis执行流程的深度剖析,希望你能感受到框架设计的精妙之处,并在未来的开发中多一份从容与自信。