MyBatis 执行流程全解析

候选人小李去美团面L7,面试官看了他简历上写的"深入了解 MyBatis 框架原理",问道:

"来,给我走一遍 MyBatis 执行一条 select 语句的完整流程。从你调用 mapper.findById(1) 开始,到返回结果为止。"

小李说:"就是调用 SqlSession,然后查缓存,然后执行 SQL,最后返回结果。"

面试官眉头一皱:"那缓存是怎么查的?Executor 里面做了什么?StatementHandler 是怎么创建的?BoundSql 里的 SQL 怎么拼出来的?"

小张开始语无伦次,在每个环节都只能说个大概...

【面试官心理】 这道题是 MyBatis 面试的核心题,能完整走通的人不超过 30%。我追问的每个细节都是有目的的:BoundSql 是动态 SQL 的载体,StatementHandler 的创建过程涉及 MyBatis 的插件机制,ResultSet 的处理过程体现了结果映射的灵活性。能把这条路走通的人,基本都看过源码。

一、从一次调用开始 🔴

先看最常见的代码:

SqlSession sqlSession = sqlSessionFactory.openSession();
UserMapper mapper = sqlSession.getMapper(UserMapper.class);
User user = mapper.findById(1L);

这一行 mapper.findById(1L) 背后,到底发生了什么?

flowchart TD
    A[mapper.findById(1L)] --> B[MapperProxy.invoke]
    B --> C[MapperMethod.execute]
    C --> D[SqlSession.selectOne]
    D --> E[CachingExecutor.query]
    E --> F[查二级缓存]
    F -->|未命中| G[SimpleExecutor.query]
    G --> H[prepareStatement 创建 PreparedStatement]
    H --> I[StatementHandler 创建]
    I --> J[ParameterHandler.setParameters]
    J --> K[JDBC 执行]
    K --> L[ResultSetHandler.handleResultSets]
    L --> M[返回 User 对象]

二、Step 1 — getMapper 是怎么工作的?

先别急着往下跳,sqlSession.getMapper(UserMapper.class) 这一步就已经有东西了。

// DefaultSqlSession.getMapper
public <T> T getMapper(Class<T> type) {
    return configuration.getMapper(type, this);
}

// Configuration.getMapper
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
    // 从 mapperRegistry 中获取 MapperProxyFactory
    MapperProxyFactory<T> mapperProxyFactory =
        (MapperProxyFactory<T>) knownMappers.get(type);
    if (mapperProxyFactory == null) {
        throw new BindingException("...");
    }
    // 创建代理对象
    return mapperProxyFactory.newInstance(sqlSession);
}

// MapperProxyFactory.newInstance
protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(
        mapperProxyFactory.getClass().getClassLoader(),
        new Class[]{mapperProxyFactory.getMapperInterface()},
        mapperProxy
    );
}

所以 getMapper 返回的是一个 MapperProxy。这个代理对象持有 SqlSession 的引用。当你调用 mapper.findById(1L) 时,代理拦截了这个调用。

💡

为什么 MyBatis 要用代理?因为它想让你写接口而不是实现类。代理在调用方法时,根据方法名 + 参数去 Configuration 里找到对应的 SQL 语句,然后执行。这就是"接口即 SQL"的精髓。

三、Step 2 — MapperProxy 拦截了什么?

public class MapperProxy<T> implements InvocationHandler {
    private final SqlSession sqlSession;
    private final Map<Method, MapperMethod> methodCache;  // 缓存已解析的 MapperMethod

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 如果是 Object 方法(equals/toString),直接调用
        if (Object.class.equals(method.getDeclaringClass())) {
            return method.invoke(this, args);
        }
        // 从缓存获取或新建 MapperMethod
        final MapperMethod mapperMethod = cachedMapperMethod(method);
        // 执行 MapperMethod
        return mapperMethod.execute(sqlSession, args);
    }
}

MapperMethod.execute 根据 SQL 类型(select/update/insert/delete)分发到不同的 SqlSession 方法:

public Object execute(SqlSession sqlSession, Object[] args) {
    Object result;
    switch (command.getType()) {
        case SELECT:
            // 处理返回结果
            result = sqlSession.selectOne(command.getName(), args);
            break;
        case INSERT:
            result = rowsAffected = sqlSession.insert(command.getName(), args);
            break;
        case UPDATE:
            result = rowsAffected = sqlSession.update(command.getName(), args);
            break;
        case DELETE:
            result = rowsAffected = sqlSession.delete(command.getName(), args);
            break;
    }
    return result;
}

四、Step 3 — SqlSession 怎么找到 SQL?

// DefaultSqlSession.selectOne
public <E> List<E> selectList(String statement, Object parameter) {
    MappedStatement ms = configuration.getMappedStatement(statement);
    return executor.query(ms, parameter, RowBounds.DEFAULT, Executor.NO_RESULT_HANDLER);
}

关键在 MappedStatement

// MappedStatement 包含了 SQL 的所有元信息
public final class MappedStatement {
    private String id;                    // namespace + methodName
    private SqlCommandType commandType;   // SELECT/INSERT/UPDATE/DELETE
    private SqlSource sqlSource;           // SQL 语句来源(动态 SQL 解析后)
    private BoundSql boundSql;            // 绑定后的 SQL(包含参数映射)
    private Cache cache;                  // 二级缓存
    private ParameterMap parameterMap;     // 参数映射
    private List<ResultMap> resultMaps;    // 结果映射
    // ...
}

五、Step 4 — Executor 查询流程 🔴

// CachingExecutor.query
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds,
                          ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) {
    // 1. 查二级缓存
    BoundSql msBoundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, msBoundSql);
    if (ms.hasCache()) {
        Cache cache = ms.getCache();
        // 从TransactionalCacheManager中获取(后面讲缓存会详细说)
        ...
    }
    // 2. 缓存未命中,委托给被包装的 Executor
    return delegate.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
}

// SimpleExecutor.doQuery —— 核心方法
public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds,
                            ResultHandler resultHandler, BoundSql boundSql) {
    // 1. 获取数据库连接
    Connection connection = transaction.getConnection();
    // 2. 创建 StatementHandler(这里是插件链的入口)
    StatementHandler handler = configuration.newStatementHandler(
        this, ms, parameter, rowBounds, resultHandler, boundSql);
    // 3. 预处理:创建 PreparedStatement 并设置参数
    Statement stmt = prepareStatement(handler, connection);
    // 4. 执行查询并处理结果
    return handler.<E>query(stmt, resultHandler);
}

// 关键:prepareStatement 方法
private Statement prepareStatement(StatementHandler handler, Connection connection) {
    // 这里会触发 ParameterHandler 设置参数
    handler.parameterize(connection.prepareStatement(sql));
    return stmt;
}

面试官心理追问:为什么 PreparedStatement 的创建要包装在 prepareStatement 方法里?

因为 connection.prepareStatement(sql) 需要数据库连接,而连接是从 Transaction 获取的。这个方法封装了"获取连接 → 创建 Statement → 设置参数"的全流程。

六、Step 5 — StatementHandler 的创建和插件链

// Configuration.newStatementHandler
public StatementHandler newStatementHandler(Executor executor, MappedStatement ms,
                                             Object parameter, RowBounds rowBounds,
                                             ResultHandler resultHandler, BoundSql boundSql) {
    // 创建基础 StatementHandler(这里走了路由)
    StatementHandler handler = new RoutingStatementHandler(executor, ms, parameter,
                                                             rowBounds, resultHandler, boundSql);

    // 插件在这里拦截!层层包装
    handler = (StatementHandler) interceptorChain.pluginAll(handler);
    return handler;
}

RoutingStatementHandler 是一个路由处理器,它根据 StatementType 决定用哪个具体的 Handler:

public class RoutingStatementHandler implements StatementHandler {
    private final StatementHandler delegate;

    public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter,
                                    RowBounds rowBounds, ResultHandler resultHandler,
                                    BoundSql boundSql) {
        // 根据 ms.getStatementType() 选择具体实现
        switch (ms.getStatementType()) {
            case STATEMENT:
                delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case PREPARED:
                delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            case CALLABLE:
                delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
                break;
            default:
                throw new ExecutorException("...");
        }
    }
}
⚠️

面试高频翻车点:很多人不知道 RoutingStatementHandler 的存在,以为直接 new 的是 PreparedStatementHandler。RoutingStatementHandler 是 MyBatis 3.4.6 引入的,之前版本是直接判断 StatementType 然后 new 对应的实现类。

七、Step 6 — ParameterHandler 设置参数

// PreparedStatementHandler.parameterize
public void parameterize(Statement statement) throws SQLException {
    // 委托给 ParameterHandler
    parameterHandler.setParameters((PreparedStatement) statement);
}

// DefaultParameterHandler.setParameters
public void setParameters(PreparedStatement ps) {
    // 遍历 BoundSql 中的每个参数
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping pm = parameterMappings.get(i);
        Object value;
        // 从参数对象中取出对应的值
        if (boundSql.hasAdditionalParameters()) {
            value = boundSql.getAdditionalParameter(pm.getProperty());
        } else if (parameterObject == null) {
            value = null;
        } else {
            // 这里用了 TypeHandlerRegistry 来找到正确的 TypeHandler
            value = typeHandlerRegistry.getTypeHandler(pm.getJavaType(), pm.getJdbcType())
                                       .getResult(rs, pm.getProperty());
        }
        // 使用对应的 TypeHandler 写入 PreparedStatement
        typeHandler.setParameter(ps, i + 1, value, pm.getJdbcTypeIndex());
    }
}

八、Step 7 — ResultSetHandler 处理结果

// PreparedStatementHandler.query
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();  // 执行 SQL
    return resultSetHandler.<E>handleResultSets(ps);  // 处理结果集
}

// DefaultResultSetHandler.handleResultSets
public List<Object> handleResultSets(Statement statement) throws SQLException {
    final List<Object> multipleResults = new ArrayList<>();
    int resultSetCount = 0;

    // JDBC 允许一次返回多个 ResultSet
    ResultSetWrapper rsw = new ResultSetWrapper(ps.getResultSet(), configuration);

    // 遍历 ResultMap(一个查询可能对应多个 ResultMap)
    for (ResultMap resultMap : resultMaps) {
        Object resultObject = resultSetHandler.wrapDefaultResultObject(rsw, resultMap,
                                                                         resultHandler,
                                                                         resultMappingContext);
        multipleResults.add(resultObject);
        resultSetCount++;
    }
    return multipleResults;
}

九、❌ 错误示范

翻车点一:说成了一条直线

"就是调用 getMapper 获取代理对象,然后执行 SQL,返回结果。"

完全没有细节,面试官随便追问一个环节就崩。

翻车点二:混淆了 BoundSql 和 MappedStatement

"SQL 存在 MappedStatement 里,BoundSql 就是绑定参数的..."

BoundSql 是 MappedStatement 经过"参数绑定"后的产物,不是一回事。BoundSql 包含了经过 #{}/${} 解析后的最终 SQL 和参数映射。

翻车点三:不知道插件链在哪个环节介入

"插件在 Executor 执行前拦截..."

插件拦截点在 StatementHandler 的创建阶段,具体来说是 configuration.newStatementHandler() 的时候,调用 interceptorChain.pluginAll(handler),所有插件对 handler 层层包装。

十、标准回答

P5 级别:能走通主流程

调用 mapper.findById(1L) 时,MyBatis 首先通过 JDK 动态代理获取 MapperProxy 对象。代理的 invoke 方法根据方法名找到对应的 MappedStatement,然后调用 SqlSession.selectOne。SqlSession 将查询委托给 Executor(先查二级缓存,未命中则走 SimpleExecutor)。Executor 通过 StatementHandler 创建 PreparedStatementParameterHandler 负责设置参数,JDBC 执行后 ResultSetHandler 将结果映射成 Java 对象返回。

P6 级别:能说出关键节点和源码细节

关键流程节点:① getMapper 返回 MapperProxy(JDK 动态代理);② MapperProxy.invoke 找到 MapperMethod 并执行;③ MapperMethod.execute 根据 SQL 类型分发到 SqlSession 的对应方法;④ CachingExecutor 先查二级缓存,缓存未命中则委托 SimpleExecutor;⑤ StatementHandler(由 RoutingStatementHandler 根据 StatementType 选择具体实现)创建 PreparedStatement;⑥ ParameterHandler 通过 TypeHandlerRegistry 获取对应的 TypeHandler 设置参数;⑦ JDBC 执行后 ResultSetHandler 通过 ResultMappingResultSet 映射成对象。BoundSql 是 MappedStatement 经过 #{}/${} 解析后的产物,包含最终 SQL 和参数映射信息。

P7 级别:能讲清设计模式和扩展机制

整个执行链路体现了 MyBatis 的分层架构和可插拔设计。MapperProxy 用 JDK 动态代理实现了"接口即 SQL";CachingExecutor 用装饰器模式在不修改 SimpleExecutor 的情况下透明添加了二级缓存;InterceptorChain.pluginAll 用责任链模式让插件可以拦截 StatementHandlerParameterHandlerResultSetHandlerExecutor 四类组件的任意方法。生产中容易踩的坑是:插件的拦截顺序是逆序的(最后添加的先拦截),而且插件必须正确处理 InvocationContext.proceed() 否则后续链会断裂。

【面试官心理】 面试官追问执行流程,其实是在验证三个能力:①你有没有看过源码;②你能不能把一个完整流程讲清楚而不是只说片段;③你能否理解 MyBatis 的设计意图。P6 的标准是能把主要节点串起来,P7 的标准是能讲清楚"为什么这么设计"和"哪些点可以扩展"。

十一、追问升级 🟡

追问1:BoundSql 是什么?它和 MappedStatement 的区别是什么?

BoundSql 是 MappedStatement 经过动态 SQL 解析后的产物:

public class BoundSql {
    private final String sql;                    // 最终的 SQL(#{} 被替换为 ?)
    private final List<ParameterMapping> parameterMappings;  // 参数映射
    private final Object parameterObject;        // 原始参数对象
    private final Map<String, Object> additionalParameters;  // 额外参数(_parameter, _databaseId 等)
}

MappedStatement 里的 SQL 是这样的:SELECT * FROM user WHERE id = #{id}

BoundSql 里的 SQL 是这样的:SELECT * FROM user WHERE id = ?

区别:BoundSql 是运行时产物,包含了占位符和参数映射;MappedStatement 是编译时配置,包含了原始 SQL 和元信息。

追问2:插件拦截的是哪个对象?

MyBatis 插件可以拦截四类对象:

拦截对象可拦截方法常见用途
Executorupdate、query、flushStatements、closeSQL 日志、慢查询记录、分页
StatementHandlerprepare、parameterize、query、update、batchSQL 改写、性能监控
ParameterHandlergetParameterObject、setParameters参数加密解密
ResultSetHandlerhandleResultSets、handleOutputParameters结果集处理

十二、生产避坑

坑一:插件拦截顺序导致的问题

// 插件执行顺序是逆序(后进先出)
@Intercepts({
    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})
})
public class PluginA implements Interceptor { }  // 先执行

@Intercepts({
    @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})
})
public class PluginB implements Interceptor { }  // 后执行,但先拦截

// 配置顺序:<plugin ref="PluginA"/> 在前,<plugin ref="PluginB"/> 在后
// 实际执行顺序:PluginB.proceed() -> PluginA.proceed() -> 原始方法 -> PluginA.after() -> PluginB.after()
⚠️

如果插件没有正确调用 invocation.proceed(),整个调用链就断了,SQL 不会执行。这是生产中最容易出的插件 bug。

坑二:StatementHandler 插件拦截不到原始 SQL

因为 RoutingStatementHandler 先创建,然后被插件包装。所以你拦截到的是已经被路由后的具体 PreparedStatementHandler。如果需要获取原始 SQL,应该从 BoundSql 中取:

// 正确方式:从 Invocation 中获取 BoundSql
StatementHandler sh = (StatementHandler) invocation.getTarget();
BoundSql boundSql = sh.getBoundSql();
String sql = boundSql.getSql();