本章我们将深入 MyBatis 的内部结构,通过源码和流程图,看清一次 SQL 执行的全貌。

2.1 架构总览:四大核心组件

MyBatis 的架构可以分为三层:接口层核心处理层基础支撑层

组件职责

  • SqlSession:门面接口,提供增删改查方法。

  • Executor:执行器,调度 StatementHandler、缓存、事务。

  • StatementHandler:封装 JDBC Statement 操作。

  • ParameterHandler:参数处理。

  • ResultSetHandler:结果集映射。

  • Configuration:全局配置,持有所有元数据。

  • TypeHandler:Java 类型 <-> JDBC 类型转换。

  • Transaction:事务抽象。

  • DataSource:数据源。

  • Cache:一级/二级缓存。

2.2 启动流程:SqlSessionFactory 的构建

关键源码XMLConfigBuilder.parseConfiguration() 依次解析各个配置节点,最终生成 Configuration 对象。

2.3 会话创建:SqlSession 的生命周期

// DefaultSqlSessionFactory.java
private SqlSession openSessionFromDataSource(ExecutorType execType, 
                                             TransactionIsolationLevel level, 
                                             boolean autoCommit) {
    Transaction tx = null;
    try {
        final Environment environment = configuration.getEnvironment();
        TransactionFactory transactionFactory = environment.getTransactionFactory();
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 创建 Executor
        final Executor executor = configuration.newExecutor(tx, execType);
        // 返回 DefaultSqlSession
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } catch (Exception e) {
        closeTransaction(tx);
        throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);
    }
}

SqlSession 内部持有 Executor,而 Executor 可能包含一级缓存。因此 SqlSession 是非线程安全的,最佳实践是每次数据库操作都新建,使用完后立即关闭。

2.4 Mapper 代理:从接口到 SQL

当调用 sqlSession.getMapper(UserMapper.class) 时,返回的是 MapperProxy 的代理对象。

// MapperProxy.java
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    if (Object.class.equals(method.getDeclaringClass())) {
        return method.invoke(this, args);
    }
    // 缓存 MapperMethod
    final MapperMethod mapperMethod = cachedMapperMethod(method);
    // 执行
    return mapperMethod.execute(sqlSession, args);
}

MapperMethod.execute() 根据 SQL 类型(INSERT、UPDATE、DELETE、SELECT)调用 sqlSession 的对应方法,并处理返回值转换。

2.5 一次查询的全流程(带源码分析)

我们以 UserMapper.selectById(1L) 为例,跟踪核心源码。

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

executor 可能是 SimpleExecutorReuseExecutorBatchExecutor,也可能是被插件代理后的对象。我们看 BaseExecutor 的模板方法:

// BaseExecutor.java
public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, 
                         ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
    if (queryStack == 0 && ms.isFlushCacheRequired()) {
        clearLocalCache();
    }
    List<E> list;
    try {
        queryStack++;
        // 从一级缓存获取
        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);
        }
    } finally {
        queryStack--;
    }
    if (queryStack == 0) {
        for (DeferredLoad deferredLoad : deferredLoads) {
            deferredLoad.load();
        }
        deferredLoads.clear();
        if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
            clearLocalCache();
        }
    }
    return list;
}
2.5.2 queryFromDatabase 调用 doQuery

queryFromDatabase 最终调用子类的 doQuery。以 SimpleExecutor 为例:

// SimpleExecutor.java
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();
        // 创建 StatementHandler
        StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, 
                                                                      rowBounds, resultHandler, boundSql);
        // 准备 Statement(通过 Transaction 获取 Connection)
        stmt = prepareStatement(handler, ms.getStatementLog());
        // 执行查询
        return handler.query(stmt, resultHandler);
    } finally {
        closeStatement(stmt);
    }
}
2.5.3 StatementHandler 处理流程

StatementHandler 的 query() 由子类 PreparedStatementHandler 实现:

// PreparedStatementHandler.java
public <E> List<E> query(Statement statement, ResultHandler resultHandler) throws SQLException {
    PreparedStatement ps = (PreparedStatement) statement;
    ps.execute();
    // 处理结果集
    return resultSetHandler.handleResultSets(ps);
}
2.5.4 参数设置(ParameterHandler)

在 prepareStatement 阶段会调用 handler.parameterize(stmt)

// DefaultParameterHandler.java
public void setParameters(PreparedStatement ps) {
    // 遍历参数映射,使用 TypeHandler 设置值
    for (int i = 0; i < parameterMappings.size(); i++) {
        // ...
        typeHandler.setParameter(ps, i + 1, value, jdbcType);
    }
}
2.5.5 结果集映射(ResultSetHandler)

DefaultResultSetHandler 的 handleResultSets() 会遍历 ResultSet,根据 ResultMap 创建对象并填充属性。

2.6 设计模式全景图

设计模式 体现 源码位置
Builder 模式 解析配置构建 Configuration XMLConfigBuilderXMLMapperBuilder
工厂模式 创建 SqlSession、Executor 等 SqlSessionFactoryConfiguration.newExecutor()
代理模式 Mapper 接口实现、插件拦截 MapperProxyPlugin
模板方法模式 定义算法骨架,子类实现细节 BaseExecutorBaseStatementHandler
策略模式 根据 Executor 类型选择执行策略 Executor 的三种实现
装饰器模式 为 Executor 增加二级缓存 CachingExecutor
责任链模式 多个插件按顺序拦截 InterceptorChain

2.7 本章小结

  • MyBatis 的启动过程是解析配置文件构建 Configuration

  • SqlSession 是非线程安全的,生命周期应短。

  • Mapper 接口通过动态代理将方法调用转换为 SQL 执行。

  • 一次查询涉及 ExecutorStatementHandlerParameterHandlerResultSetHandler 的协作。

  • 框架运用了多种设计模式,具备良好的扩展性。


面试题

1. MyBatis 的 Executor 三种类型分别适用于什么场景?BATCH 模式下为何无法获取自增主键?

参考思路

  • SIMPLE:默认,每次新建 Statement,适合普通场景。

  • REUSE:复用 PreparedStatement,适合循环执行同构 SQL。

  • BATCH:批量执行,适合批量插入/更新,性能高。

  • BATCH 无法获取自增主键的原因:JDBC 的 executeBatch() 返回的是 int[] 影响行数,数据库返回的主键无法在此过程中获取。如需主键,可改用逐条插入或使用支持批量返回主键的数据库驱动特性(如 MySQL 的 rewriteBatchedStatements=true)。

2. 请描述从 Mapper 接口方法调用到 SQL 执行的完整调用链。

参考思路
Mapper 接口方法 → MapperProxy.invoke() → MapperMethod.execute() → SqlSession.selectOne() → Executor.query() → StatementHandler.query() → ParameterHandler.setParameters() → Statement.execute() → ResultSetHandler.handleResultSets()

3. SqlSession 为什么是线程不安全的?在 Spring 整合后如何保证?

参考思路

  • SqlSession 内部持有 ExecutorExecutor 包含一级缓存和延迟加载队列,多线程共享会导致数据混乱。

  • Spring 整合使用 SqlSessionTemplate,它通过 ThreadLocal 为每个线程绑定独立的 SqlSession,方法执行完毕后自动关闭,从而保证线程安全。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐