AI摘要

MyBatis执行SQL全流程:SqlSessionFactory解析配置生成MappedStatement;SqlSession.getMapper创建代理,MapperProxy.invoke转调MapperMethod.execute;DefaultSqlSession委托Executor,先查一级缓存,未命中则SimpleExecutor.doQuery,StatementHandler创建PreparedStatement,ParameterHandler设参,ResultSetHandler映射结果并返回。

在日常开发中,我们早已习惯了这样的写法:

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初始化的核心。它会:

  1. 解析mybatis-config.xml全局配置文件。
  2. 加载所有的*Mapper.xml文件,并将每一个<select|insert|update|delete>标签解析成一个MappedStatement对象。
  3. 这个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)时,实际上调用的是ProxyInvocationHandlerinvoke方法。在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中定义的resultTyperesultMap,通过反射,将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

为什么这很重要?

理解这个过程,能让你在面对日常开发中的许多“灵异事件”时,不再束手无策。

  • 参数映射错误? 你知道该去查ParameterHandlerTypeHandler
  • 结果映射失败? 你的目标是ResultSetHandler和对应的ResultMap配置。
  • 一级缓存导致数据不一致? 你明白是因为同一个SqlSession内的查询复用了缓存,知道该在合适的时候调用sqlSession.clearCache()
  • 插件(分页、日志)如何工作? 你清楚它们是在Executor被调用前,通过动态代理插入了自己的逻辑。

源码阅读不是目的,而是手段。通过这次对MyBatis执行流程的深度剖析,希望你能感受到框架设计的精妙之处,并在未来的开发中多一份从容与自信。

版权声明 ▶ 本网站名称:黄磊的博客
▶ 本文标题:MyBatis源码解读:一条SQL语句是如何被执行的?
▶ 本文链接:https://www.huangleicole.com/backend-related/74.html
▶ 转载本站文章需要遵守:商业转载请联系站长,非商业转载请注明出处!!

如果觉得我的文章对你有用,请随意赞赏