乐于分享
好东西不私藏

MyBatis SQL 执行流程源码详解 —— 从调用到数据库的完整流程

MyBatis SQL 执行流程源码详解 —— 从调用到数据库的完整流程

先看完整执行流程

二、逐层拆解:每一步到底发生了什么

第 1 步:调用 Mapper 接口 —— 代理的魔法

接口代码:

public interface DesignWorkHourFillDao extends BaseMapper<DesignWorkHourFill> {    List<DeptYearHour> getDeptYearHour();    List<ShipHour> getAllByProjectIds(@Param("projectIds") List<String> projectIds);    IPage<CostPredictDesignHourDto> pageHour(IPage<CostPredictDesignHourDto> page,@Param("year")int year);    List<CostPredictDesignHourDto> listHour(@Param("year")int year);    List<CostPredictDesignHourDto> actualHour(@Param("year")int year);    List<ProjectNoName> noNameMap();    List<YearHour> yearHour(int year);}

调用它:

List<ProjectNoName>projectNoNames=mapper.noNameMap();

MyBatis 通过 JDK 动态代理 生成了一个 MapperProxy

核心源码逻辑(简化):

// MapperProxy.invoke()public Object invoke(Object proxy, Method method, Object[] args) {    // 1. 如果是 Object 的方法(toString 等),直接执行    if (Object.class.equals(method.getDeclaringClass())) {        return method.invoke(thisargs);    }    // 2. 从缓存中获取 MapperMethod    MapperMethod mapperMethod = cachedMapperMethod(method);    // 3. 委托给 MapperMethod 执行    return mapperMethod.execute(sqlSession, args);}

第 2 步:SqlSession —— 门面角色,不干重活

SqlSession 是用户操作数据库的门面,它不直接执行 SQL,而是将请求委托给 Executor:

// DefaultSqlSession.selectList()public <E> List<E> selectList(String statement, Object parameter) {    // 从 Configuration 中获取 MappedStatement    MappedStatement ms = configuration.getMappedStatement(statement);    // 委托给 Executor 执行    return executor.query(ms, wrapCollection(parameter),                           RowBounds.DEFAULTExecutor.NO_RESULT_HANDLER);}
职责SqlSessionExecutor
事务管理✅ 提交/回滚
获取 MappedStatement
执行 SQL❌ 委托出去
缓存管理
插件拦截

获取sqlSession过程

String resource = "mybatis‐config.xml";         Reader reader;         try {             //将XML配置文件构建为Configuration配置类             reader = Resources.getResourceAsReader(resource);             // 通过加载配置文件流构建一个SqlSessionFactory DefaultSqlSessionFactory             SqlSessionFactory sqlFactory = new SqlSessionFactoryBuilder().build(reader);             // 数据源 执行器 DefaultSqlSession             SqlSession session = sqlFactory.openSession();             try {                 // 执行查询 底层执行jdbc                 DesignWorkHourFillDao mapper = session.getMapper(DesignWorkHourFillDao.class);                 System.out.println(mapper.getClass());                 List<ProjectNoName> projectNoNames = mapper.noNameMap();                 for (ProjectNoName projectNoName : projectNoNames) {                     System.out.println(projectNoName);                 }                 } catch (Exception e) {                 e.printStackTrace();                 }finally {                 session.close();                 }             } catch (IOException e) {             e.printStackTrace();             }@Override  public SqlSession openSession() {    return openSessionFromDataSource(configuration.getDefaultExecutorType(), nullfalse);  }private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {    Transaction tx = null;    try {      final Environment environment = configuration.getEnvironment();      final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);      tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);        ///获取执行器,这边获得的执行器已经代理拦截器的功能      final Executor executor = configuration.newExecutor(tx, execType);        //根据创建的执行器创建sqlSession      return new DefaultSqlSession(configuration, executor, autoCommit);    } catch (Exception e) {      closeTransaction(tx); // may have fetched a connection so lets call close()      throw ExceptionFactory.wrapException("Error opening session.  Cause: " + e, e);    } finally {      ErrorContext.instance().reset();    }  }

第 3-4 步:Executor —— 执行器的三层组装

这是 MyBatis 设计的精髓。Executor 的创建经历了 策略选择 → 缓存装饰 → 插件代理 三层组装:

代码:

//获取执行器的代码//将最终组装好的 executor(经过了 类型选择 → 缓存装饰 → 插件代理 三个步骤)返回给调用者(通常是 SqlSession)。public Executor newExecutor(Transaction transaction, ExecutorType executorType) {    executorType = executorType == null ? defaultExecutorType : executorType;    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;    Executor executor;    if (ExecutorType.BATCH == executorType) {        //批处理模式 — 如果类型是 BATCH,创建 BatchExecutor。它会攒多条相同 SQL 的 addBatch() 调用,最后一次性 executeBatch() 发送给数据库,适合批量 INSERT/UPDATE/DELETE 场景(JDBC 的  addBatch/executeBatch)。      executor = new BatchExecutor(this, transaction);    } else if (ExecutorType.REUSE == executorType) {        //复用模式 — 如果是 REUSE,创建 ReuseExecutor。它会缓存 PreparedStatement,相同 SQL 不会重复编译(prepareStatement),而是复用之前的 Statement 对象,减少 SQL 预编译开销。      executor = new ReuseExecutor(this, transaction);    } else {        //默认/简单模式 — 其他情况(即 SIMPLE)创建 SimpleExecutor。每次执行都创建新的 Statement,用完即关,行为最直观,适合大多数场景。      executor = new SimpleExecutor(this, transaction);    }    if (cacheEnabled) {      executor = new CachingExecutor(executor);    }    //插件拦截链 — 将配置中的所有 MyBatis 插件(Interceptor)应用到 executor 上。MyBatis 插件的本质是 JDK 动态代理,pluginAll() 会返回一个层层代理包装后的对象。这样用户自定义的拦截器(比如分页插件、SQL日志、读写分离等)就能拦截 Executor 的 query、update、commit 等方法。    executor = (Executor) interceptorChain.pluginAll(executor);    return executor;  }

三种基础执行器的区别

执行器特点适用场景
SimpleExecutor每次执行创建新 Statement,用完关闭日常查询(默认)
ReuseExecutor执行update或select,以sql作为key查找Statement对象,存在就使用,不存在就创建,用完后,不关闭Statement对象,而是放置于Map<String, Statement>内,供下一次使用。简言之,就是重复使用Statement对象。频繁重复 SQL 查询
BatchExecutor攒多条 SQL,调用 executeBatch 批量发送大批量 INSERT/UPDATE

Executor分成两大类,一类是CacheExecutor,另一类是普通Executor。普通Executor又分为三种基本的Executor执行器,SimpleExecutor,ReuseExecutor、BatchExecutor。

CacheExecutor其实是封装了普通的Executor,和普通的区别是在查询前先会查询缓存中是否存在结果,如果存在就使用缓存中的结果,如果不存在还是使用普通的Executor进行查询,再将查询出来的结果存入缓存。

第 5 步:缓存检查 —— 二级缓存与一级缓存

两张缓存的区别:

维度一级缓存二级缓存
作用域SqlSession 级别namespace(Mapper)级别
默认状态默认开启,无法关闭默认开启但需手动配置<cache/>
生命周期随 SqlSession 关闭而清空随应用运行一直存在
共享范围同一 SqlSession 内跨 SqlSession、跨线程
存储位置JVM 堆内存(Map)可配置 Ehcache / Redis 等
清空时机update / commit / closeupdate / <cache-ref> / 手动清空

⚠️ 注意:在 Spring 环境下,每次查询 SqlSession 都是新的,一级缓存几乎失效。此时应优先考虑业务层面的缓存方案。


第 6 步:创建 StatementHandler —— 又一个策略模式

// Configuration.newStatementHandler()public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {    StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);    statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);    return statementHandler;  }

RoutingStatementHandler 根据 MappedStatement.statementType 路由到不同实现:

项目中 99% 的情况都是 PreparedStatementHandler,它是默认且最安全的(防止 SQL 注入)。


第 7-9 步:参数处理与 SQL 执行

BoundSql 是这里的关键对象,它里面保存了三样东西:

以如下例子进行说明

<selectid="selectUser">    SELECT id,name,age FROM user    WHERE name = #{userName} AND age = #{userAge}    AND create\_time = ${createTime}</select>
public classBoundSql{    /**    解析完成后的纯预处理 SQL    - 所有 `#{xxx}` 全部替换为 JDBC 占位符 `?``${xxx}` 直接替换成对应字符串(直接拼接,无占位符)	- 去掉 XML 中的换行、多余空格,生成可给 `PreparedStatement` 使用的 SQL	如:SELECT id,name,age FROM user WHERE name = ? AND age = ? AND create_time = '2026-07-26'    */  private final String sql;     /**    占位符 `?` 对应的参数元数据列表**	SQL 里有几个`?`,集合里就有几个`ParameterMapping`对象。	每个`ParameterMapping`记录:占位符对应的参数名称、参数类型、是否允许 null 等信息。	ParameterMapping 内部关键属性:`property`:参数名(userName /userAge)`javaType`:参数 Java 类型(String / Integer)`jdbcType`:数据库字段类型(VARCHAR / INT)    */  private final List<ParameterMapping> parameterMappings;    /**    用户传入的主参数对象	就是调用 Mapper 方法时传入的参数,可以是实体类、Map、普通基础类型。    */  private final Object parameterObject;    /**    附加参数容器,存放不属于主 parameterObject 的额外变量	典型来源:1`<foreach>` 循环变量(item、index2`${}` 表达式自定义变量3. MyBatis 内置分页参数、自定义上下文参数    */  private final Map<String, Object> additionalParameters;    /**    additionalParameters的包装工具对象,用于便捷读取 / 修改附加参数 Map`MetaObject` 是 MyBatis 内置反射工具,专门用来:		- 快速 get/set Map、实体对象的属性,不用手动反射		- 统一处理参数取值逻辑,封装反射细节    */  private final MetaObject metaParameters;}

TypeHandler 的转换逻辑:

数据库类型Java 类型TypeHandler
VARCHAR / CHARStringStringTypeHandler
INTEGER / INTInteger / intIntegerTypeHandler
BIGINTLong / longLongTypeHandler
DECIMALBigDecimalBigDecimalTypeHandler
DATE / DATETIMEjava.util.DateDateTypeHandler
TIMESTAMPjava.time.LocalDateTimeLocalDateTimeTypeHandler
自定义枚举Enum自定义 EnumTypeHandler

第 10-12 步:结果集映射

映射规则(项目中 mapUnderscoreToCamelCase=true):

数据库列名                          Java 属性名────────────────────────────────────────────project_code       →    自动映射    →   projectCodedelivery_date      →    自动映射    →   deliveryDateship_type          →    自动映射    →   shipTypegross_profit       →    自动映射    →   grossProfit

三、穿插一个关键角色:插件拦截链

MyBatis 四大组件都可以被插件拦截:

最经典的应用就是分页插件 PageHelper:

一个自定义的简单分页插件

@Intercepts({        @Signature(                type = Executor.class,                method = "query",                args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}        )})public class PseudoPageInterceptor implements Interceptor {    @Override    public Object intercept(Invocation invocation) throws Throwable {        Object[] args = invocation.getArgs();        MappedStatement ms = (MappedStatement) args[0];        Object parameter = args[1];        RowBounds rowBounds = (RowBounds) args[2];        // 这里写死演示分页参数        int offset = rowBounds.getOffset();        int limit = 10;        // 1. 获取原始 BoundSql(包含 SQL + 参数映射)        BoundSql boundSql = ms.getBoundSql(parameter);        String originalSql = boundSql.getSql();        String pagedSql = originalSql + " LIMIT " + limit + " OFFSET " + offset;        System.out.println("[PseudoPage] ─────────────────────────────");        System.out.println("  原始 SQL : " + originalSql.replaceAll("\\s+"" ").trim());        System.out.println("  分页 SQL : " + pagedSql.replaceAll("\\s+"" ").trim());        System.out.println("  Offset   : " + offset + ", Limit: " + limit);        // :替换 SqlSource,这样无论谁调 getBoundSql() 都能拿到分页 SQL。        SqlSource newSqlSource = new BoundSqlSqlSource(                ms.getConfiguration(), pagedSql, boundSql.getParameterMappings(), parameter);        replaceSqlSource(ms, newSqlSource);        System.out.println("[PseudoPage] ✅  SqlSource 已替换,分页 SQL 会向下传递");        System.out.println("[PseudoPage] ─────────────────────────────");        // 放行 → 下一个拦截器 → CachingExecutor → BaseExecutor → 数据库        return invocation.proceed();    }    /**     * 通过反射替换 MappedStatement 中的 sqlSource 字段     */    private void replaceSqlSource(MappedStatement ms, SqlSource newSqlSource) {        try {            Field field = MappedStatement.class.getDeclaredField("sqlSource");            field.setAccessible(true);            field.set(ms, newSqlSource);        } catch (Exception e) {            System.err.println("[PseudoPage] 替换 SqlSource 失败: " + e.getMessage());        }    }    /**     * 一个简单的 SqlSource 实现:直接返回指定的 SQL 和参数映射     * 不重新解析,直接用我们构造好的分页 SQL     */    static class BoundSqlSqlSource implements SqlSource {        private final Configuration configuration;        private final String sql;        private final List<ParameterMapping> parameterMappings;        private final Object parameterObject;        BoundSqlSqlSource(Configuration configuration, String sql,                          List<ParameterMapping> parameterMappings, Object parameterObject) {            this.configuration = configuration;            this.sql = sql;            this.parameterMappings = parameterMappings;            this.parameterObject = parameterObject;        }        @Override        public BoundSql getBoundSql(Object parameterObject) {            // 优先用构造时传入的参数(原始 BoundSql 中已解析好的)            Object param = parameterObject != null ? parameterObject : this.parameterObject;            return new BoundSql(configuration, sql, parameterMappings, param);        }    }    @Override    public Object plugin(Object target) {        return Plugin.wrap(target, this);    }    @Override    public void setProperties(Properties properties) {        System.out.println("[PseudoPage] 初始化完成");    }}

拦截器包装后的调用链:

调用方 → Plugin(分页拦截器) → Plugin(日志拦截器) → 原始 Executor              ↓ 改 SQL 加 LIMIT         ↓ 打印 SQL 耗时

执行结果,成功实现分页:

四、带插件的完整执行流程

五、关键设计模式总结

设计模式出现位置作用
代理模式MapperProxy → Mapper 接口接口无实现类,动态代理拦截
门面模式SqlSession统一对外接口,屏蔽内部复杂性
策略模式Executor(Simple/Reuse/Batch)根据不同场景选择不同执行策略
装饰器模式CachingExecutor 包装基础 Executor不改变接口,动态增加缓存功能
模板方法模式BaseExecutor.query() → queryFromDatabase()固定流程骨架,子类实现具体步骤
责任链模式InterceptorChain → Plugin多个拦截器依次处理
建造者模式MappedStatement.Builder复杂对象的构建
工厂模式Configuration 各种 newXxx 方法统一创建组件

常见面试问题速答

Q1:一级缓存和二级缓存的区别?

一级缓存是 SqlSession 级别的,默认开启,无法跨会话共享;二级缓存是 namespace 级别的,需手动配置 <cache/>,可跨会话共享。

Q2:MyBatis 如何防止 SQL 注入?

使用 #{param} 占位符(PreparedStatement),参数值会被 TypeHandler 处理后通过 setXxx() 设入,不会直接拼接到 SQL 字符串中。而 ${param} 是直接字符串替换,存在注入风险。

Q3:插件的执行顺序?

InterceptorChain.pluginAll() 中按插件配置顺序依次生成代理对象。例如先配置 A 再配置 B,则调用链为:调用方 → PluginA代理 → PluginB代理 → 原始对象

Q4:#{} 和 ${} 的区别?

#{} → 预编译占位符 ?,安全、可防止注入。${} → 直接字符串拼接,有注入风险,通常用于动态表名或 ORDER BY 字段名。

Q5:MyBatis 的执行器有哪些?

SIMPLE(默认,每次新建 Statement)、REUSE(复用 Statement)、BATCH(批量执行)。可通过 <setting name="defaultExecutorType" value="BATCH"/> 配置。