在阅读该文章之前,建议先看 MyBatis核心设计模式详解,或者两篇文章结合着看,互为补充。

目前国内 Java 后端持久层最主流的三大框架是 MyBatis-Plus、Spring Data JPA 和 MyBatis-Flex,其中 MyBatis-Plus 和 MyBatis-Flex 均基于 MyBatis 增强,足以证明 MyBatis 的核心地位。

    下面我们跟着一个 SQL 请求从头到尾走一遍它的主流程。

    示例代码

    项目结构

    项目代码

    pom.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
        <modelVersion>4.0.0</modelVersion>
    
        <groupId>com.example</groupId>
        <artifactId>demo</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
            <maven.compiler.source>21</maven.compiler.source>
            <maven.compiler.target>21</maven.compiler.target>
            <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        </properties>
        <dependencies>
            <!-- MyBatis -->
            <dependency>
                <groupId>org.mybatis</groupId>
                <artifactId>mybatis</artifactId>
                <version>3.5.19</version>
            </dependency>
    
            <!-- MySQL驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.33</version>
            </dependency>
        </dependencies>
    </project>

    mybatis-config.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE configuration
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "https://mybatis.org/dtd/mybatis-3-config.dtd">
    
    <configuration>
        <!-- 环境配置 -->
        <environments default="development">
            <environment id="development">
                <transactionManager type="JDBC"/>
                <!-- MyBatis自己实现的一个简易连接池  -->
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://192.168.5.101:3306/blog_system?useSSL=false&amp;allowPublicKeyRetrieval=true&amp;serverTimezone=UTC&amp;useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true"/>
                    <property name="username" value="hostonly_user"/>
                    <property name="password" value="123456"/>
    
                    <!-- 连接池配置 -->
                    <property name="poolMaximumActiveConnections" value="10"/> <!-- 最大活跃连接数 -->
                    <property name="poolMaximumIdleConnections" value="5"/>   <!-- 最大空闲连接数 -->
                    <property name="poolMaximumCheckoutTime" value="20000"/>  <!-- 最大 checkout 时间 -->
                </dataSource>
            </environment>
        </environments>
    
        <!-- 加载Mapper映射文件 -->
        <mappers>
            <mapper resource="mapper/UserMapper.xml"/>
        </mappers>
    </configuration>

    User.java & UserMapper.java

    //entity -> User
    public class User {
        private int id;
        private String username;
        private String email;
    
        // 构造方法
        public User(String username, String email) {
            this.username = username;
            this.email = email;
        }
    
        // Getters and Setters
        // ......
    
        @Override
        public String toString() {
            return "User{id=" + id + ", username='" + username + "', email=" + email + "}";
        }
    }
    
    
    //mapper -> UserMapper
    public interface UserMapper {
        // 查(单个)
        User selectUserById(Integer id);
    
        // 查(所有)
        List<User> selectAllUsers();
    }
    

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
    
    <mapper namespace="com.example.mapper.UserMapper">
    
        <!-- 根据ID查询用户 -->
        <select id="selectUserById" resultType="com.example.entity.User">
            SELECT id, username, email
            FROM user
            WHERE id = #{id}
        </select>
    
    
        <!-- 查询所有用户 -->
        <select id="selectAllUsers" resultType="com.example.entity.User">
            SELECT id, username, email FROM user
        </select>
    
    </mapper>

    测试类

    public class Main {
        public static void main(String[] args) throws Exception {
            // 加载配置
            String resource = "mybatis-config.xml";
            InputStream inputStream = Resources.getResourceAsStream(resource);
            SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);
    
            try (SqlSession session = factory.openSession()) {
                UserMapper mapper = session.getMapper(UserMapper.class);
    
                // 查询ID为1的用户
                System.out.println("查询id为1的用户");
                User user = mapper.selectUserById(1);
                System.out.println(user);
                System.out.println("-------------------------------------------------------");
    
                System.out.println("查询所有的用户");
                List<User> users = mapper.selectAllUsers();
                users.forEach(e -> System.out.println(e.toString()));
            }
        }
    }

    测试结果:

    Debug 追踪一条 SQL 的完整执行过程

    下面以main方法中查询id为1的用户为例,一步一步debug

    1. 获取SqlSessionFactory
    // 加载配置
    //1
    String resource = "mybatis-config.xml";
    //2
    InputStream inputStream = Resources.getResourceAsStream(resource);
    //3
    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

    前两句代码把mybatis-config.xml文件读到内存,封装成字节输入流。

    重点看第三行代码

    SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(inputStream);

    这行代码分成两部分

    1. new SqlSessionFactoryBuilder()

    创建构造器建造者对象(建造者模式),专门用来建工厂

    2. build(inputStream)

    在这一行打断点debug进去,会看到调用链路如下:SqlSessionFactoryBuilder 的 build(inputStream) -> SqlSessionFactory 的 build(inputStream, environment, properties),SqlSessionFactory 的 build方法里的关键代码如下

     SqlSessionFactory var5;
     try {
         XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
         var5 = this.build(parser.parse());
     }

    创建一个XMLConfigBuilder对象, 这个对象调用parse()方法,parse()方法如下

    public Configuration parse() {
            if (this.parsed) {
                throw new BuilderException("Each XMLConfigBuilder can only be used once.");
            } else {
                this.parsed = true;
                this.parseConfiguration(this.parser.evalNode("/configuration"));
                return this.configuration;
            }
        }

    这个方法在项目启动时只加载一次,所以进入else分支,以 <configuration>作为根节点解析mybatis-config.xml,包括如下内容:

    • 解析全局配置文件(环境、数据源、连接池、驼峰、插件)
    • 解析所有 Mapper.xml/ 注解SQL
    • 把每一条 SQL(Mapper.xml里的每一个<select|insert|update|delete> 标签对应一条SQL) + 参数规则 + 结果映射规则,统一封装成 MappedStatement
    • 全部存入全局唯一的大容器:Configuration

    调试截图:

    parser.parse()执行结果返回一个Configuration。Configuration 可以理解为一个全局仓库,后面所有代理、执行器、SQL 查找、映射规则,全部从这里拿数据。我们接着看下面代码的最后一行。

    SqlSessionFactory var5;
     try {
         XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
         var5 = this.build(parser.parse());
     }

    var5 = this.build(parser.parse());

    得到Configuration之后传给this.build()方法,然后调用下面的方法

    public SqlSessionFactory build(Configuration config) {
            return new DefaultSqlSessionFactory(config);
        }

    最终创建一个SqlSessionFactory(默认实现为DefaultSqlSessionFactory)。

    public class DefaultSqlSessionFactory implements SqlSessionFactory {
        private final Configuration configuration;
    
        public SqlSession openSession() {
            return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
        }
    //......
    }

    SqlSessionFactory可以理解为“数据库操作总工厂”,持有全局配置 Configuration(单例),主要负责生产 SqlSession(数据库操作会话)。

    2. 获取SqlSession

    接着看测试类main方法的下一行代码

    try (SqlSession session = factory.openSession()) {

    上一步拿到SqlSessionFactory(默认实现为DefaultSqlSessionFactory)之后,开始从工厂获取SqlSession,它是一次与数据库的会话(连接),用来执行 SQL、提交事务、关闭连接。debug进去看,它的调用链如下

    openSession() -> openSessionFromDataSource() -> createSqlSession() -> return new DefaultSqlSession(configuration, executor, autoCommit),其中openSessionFromDataSource()的代码如下

    主要实现的功能是:1) 从配置中拿到数据源和事务工厂,2) 用事务工厂创建事务对象,3)用事务对象创建SQL 执行器,4)createSqlSession()封装成 SqlSession 并返回。

    createSqlSession() 会走到 return new DefaultSqlSession(configuration, executor, autoCommit),DefaultSqlSession的构造函数如下

    public DefaultSqlSession(Configuration configuration, Executor executor, boolean autoCommit) {
            this.configuration = configuration;
            this.executor = executor;
            this.dirty = false;
            this.autoCommit = autoCommit;
        }

    最终得到的DefaultSqlSession对象如下图所示。需要注意一点:这个session是执行数据库操作的门面,它并不直接干活,而是将具体任务交给内部的 Executor(执行器)去完成。同时SqlSession 会维护一级缓存(一级缓存的作用域是 SqlSession 级别,默认开启,在同一个 Session 中重复查询会命中缓存)。

    3. 获取代理对象

    下面接着debug,来到

    UserMapper mapper = session.getMapper(UserMapper.class);

    session.getMapper(UserMapper.class)这个方法会调用DefaultSqlSession的getMapper(),再到 

    Configuration.getMapper() ->  MapperRegistry.getMapper() -> 

    MapperProxyFactory的newInstance(sqlSession) -> newInstance(mapperProxy)。

    MapperProxyFactory的newInstance(sqlSession)的代码如下:

    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache)

    这里创建 MapperProxy。由下图可知,MapperProxy 实现了 InvocationHandler 接口,是真正处理方法调用的类。MapperProxy 的 invoke() 方法会拦截 Mapper 接口的所有自定义业务方法。

    MapperProxyFactory的newInstance(mapperProxy)的代码实现如下图:

    这个方法内部用JDK动态代理生成 UserMapper接口的代理对象,如下图。

    4. 执行SQL(从代理到Executor)

    继续调试下一行代码

    User user = mapper.selectUserById(1);

    在该行打断点,同时在MapperProxy的invoke()打一个断点,执行这行代码的时候会进入MapperProxy的invoke(),代理会拦截所有自定义业务方法调用。

    被调用的方法不是 Object 类中声明的方法,然后会执行
    this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession),代码如下:

    MapperMethodInvoker是MyBatis 封装的方法执行器,专门用来执行 Mapper 接口的方法(如 selectList、insert 等)。methodCache 是方法级缓存,key 是 Mapper 接口的 Method 对象,value 是 MapperMethodInvoker(内部封装了 MapperMethod),避免重复解析 SQL 类型和返回值类型,大幅提升性能。

    MapUtil.computeIfAbsent(methodCache, method, m -> {
    代码解析:MapUtil.computeIfAbsent缓存不存在则创建,存在直接返回,从 methodCache 中拿 method 对应的执行器,没有就创建一个放进去。

    if (!m.isDefault()),因为isDefault() = Java8 接口默认方法,Mapper 接口里的方法都不是 default,所以进 if,然后创建真正执行 SQL 的核心对象MapperMethod。

    return new PlainMethodInvoker(
        new MapperMethod(
            this.mapperInterface,  // Mapper接口
            method,                // 当前方法 - selectUserById(1)
            this.sqlSession.getConfiguration()  // 全局配置
        )
    );

    MapperMethod 封装了如下内容:SQL 语句的类型(SELECT/INSERT/UPDATE/DELETE);参数解析逻辑;结果映射逻辑;执行 SQL 的实际操作(MapperMethod 本身不执行 SQL,它是通过调用 SqlSession 的方法来完成的)等。

    获取到MapperMethod(PlainMethodInvoker 是方法执行器包装类,内部持有真正负责 SQL 调度的 MapperMethod,统一提供 invoke 入口。)之后,会走下面的invoke()方法

     @Override
     public Object invoke(Object proxy, Method method, Object[] args, SqlSession sqlSession) throws Throwable {
          return mapperMethod.execute(sqlSession, args);
     }

    然后进入MapperMethod的execute方法

    execute () 方法根据 Mapper 接口方法(select/update/insert/delete),自动判断 SQL 类型,调用对应的 SqlSession 方法,最终把结果返回。它是代理调用 → SQL 执行之间的总调度器。

    因为selectUserById的标签是select,所以来到了SELECT分支,下图展示了MapperMethod的各属性的值

    至此,请求从 JDK 动态代理层,正式进入 MyBatis 核心 SQL 执行流程。

    继续debug:

    sqlSession.selectOne(statement, parameter)  -> ... -> DefaultSqlSession.selectList(statement, parameter, rowBounds, handler),DefaultSqlSession的selectList()方法实现如下

    该方法主要做了两件事,一是从configuration获取了MappedStatement,二是把具体的数据查询任务交给了Executor(执行器)去执行,最终的查询结果通过sqlSession返回到业务调用处。正如我们前面所说的,session是执行数据库操作的门面,它并不直接干活,而是将具体任务交给内部的 Executor(执行器)去完成。

    继续看executor.query()

    方法里的ms.getBoundSql() 根据传入参数,解析动态 SQL,生成带 ? 占位符的标准 SQL 语句,并封装参数映射关系,供 JDBC PreparedStatement 使用。

    经过query(ms, parameterObject, rowBounds, resultHandler,key, boundSql)方法,然后调用BaseExecutor.query(ms, parameter, rowBounds,  resultHandler, key, boundSql) -> queryFromDatabase(ms, parameter, rowBounds,resultHandler, key, boundSql)。

    BaseExecutor.query(ms, parameter, rowBounds,  resultHandler, key, boundSql)主要代码如下,先查一级缓存,缓存没命中再走数据库查询

     List<E> list;
     try {
       //1. 默认查询(未自定义结果处理器)时,优先从一级缓存获取
       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
       if (list != null) {
       // 缓存命中,处理存储过程的 OUT 参数(常规查询可忽略)
         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
       } else {
       // 2. 缓存未命中(或者自定义了结果处理器),走数据库查询
         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
       }

    我们的SQL查询缓存没有命中,走数据库查询,调用queryFromDatabase方法

    然后进入SimpleExecutor的doQuery()

    doQuery() 是 Executor 真正执行查询的核心方法,主要做 4 件事:
    1. 创建 StatementHandler:MyBatis 核心处理器,统筹参数赋值(ParameterHandler)、SQL 执行、结果映射(ResultSetHandler)三大环节。
    2. 预处理 SQL(prepareStatement()):获取数据库连接(connection)、创建 JDBC PreparedStatement、把参数设置到 SQL 里(调用 ParameterHandler.setParameters() 将参数设置到 PreparedStatement 中,这是 MyBatis 处理 SQL 参数赋值的核心执行入口)。
    3. 执行查询(handler.query):调用 JDBC 执行 SQL 得到原始结果集,再由内部 ResultSetHandler 将结果集映射封装为 Java 对象。
    4. 关闭资源:finally 块中关闭 Statement,避免资源泄漏;连接归连接池管理,此处不关闭连接。

    上面代码里的return handler.query(stmt, resultHandler)最终调用PreparedStatementHandler的query()

    可以看到在这里调用JDBC执行SQL,然后调用DefaultResultSetHandler 的 handleResultSets(Statement stmt) 解析返回的ResultSet,代码如下

    可以看到,while循环将数据库返回的 N 个结果集(ResultSet)按照定义的 ResultMap 规则(如果xml配置的是resultType,MyBatis自动创建一个默认的 ResultMap)一行行映射成 Java 对象,最后返回 List如下图(handleResultSets方法里的第二个while循环用于处理存储过程等场景返回的多个结果集)。

    最终闭环:封装完对象 → 返回给 Mapper 代理方法 → 业务代码拿到结果。

    总结

    上面的整个debug过程可以简化为下面的流程:

    1. 加载配置:解析 mybatis-config.xml 及所有 Mapper 文件,存入全局单例 Configuration。
    2. 构建工厂:通过 SqlSessionFactoryBuilder 创建 DefaultSqlSessionFactory。
    3. 打开会话:factory.openSession() 创建 SqlSession(持有 Executor)。
    4. 获取代理:通过 JDK 动态代理生成 Mapper 接口代理对象(MapperProxy)。
    5. 调用接口方法:被 MapperProxy.invoke() 拦截,交给 MapperMethod.execute() 路由(决定是增/删/改/查)。
    6. Executor 执行:SqlSession 将操作委托给 Executor(先查询缓存 → 若未命中,则走数据库)。
    7. SQL预编译 & 设参:Executor 创建 StatementHandler,完成SQL预编译,调用 parameterize() 触发 ParameterHandler.setParameters() 为 ? 赋值。
    8. JDBC 执行:PreparedStatementHandler.query() 调用 PreparedStatement.execute(),执行 SQL,获取ResultSet。
    9. 结果集映射:同一个 query() 方法中紧接着调用 ResultSetHandler.handleResultSets(),通过反射将 ResultSet 封装为 Java 实体对象(或集合)。
    10. 结果返回:层层返回,最终代理对象将结果返回给业务调用方。

    MyBatis框架的核心类如下

    核心类 职责
    Configuration 全局配置中心,持有所有 MappedStatement及其他配置信息
    MapperProxy 动态代理,拦截 Mapper 接口方法调用
    Executor

    SQL 执行器,管理缓存、事务,调度 StatementHandler 完成数据库读写

    StatementHandler

    SQL 处理器,协调 ParameterHandler 和 ResultSetHandler,完成 SQL 执行的全流程

    ParameterHandler 封装 SQL 参数,为 PreparedStatement 完成占位符参数赋值
    ResultSetHandler 按照映射规则将 ResultSet 自动封装为 Java 对象
    Logo

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

    更多推荐