【MyBatis源码工坊】(一)MyBatis 是怎么运行的?

131 阅读18分钟

MyBatis 源码分析

前言

在介绍 MyBatis 源码之前,先分享一些自己阅读源码的感悟。

我自己阅读源码的收获

MyBatis 算是自己第一个看的比较细致的源码框架了,其实一开始学习源码的时候,看的特别细,对于整个流程中的每个方法我会画流程图,标注「类#方法」执行的流程和路径,目的是希望自己以后可以复习。

但是,后来发现自己根本没有看过,只能记住核心流程大概是怎么样的,细枝末节忘得一干二净,逐渐意识到记住代码内部具体执行的细节并不能为我带来很大的帮助,而且也根本记不住。

阅读完之后,能长期记忆的内容只有解决方案、设计思路,例如,MyBatis 内部为了解析动态 SQL 语句,设计了 SqlNode 节点树,通过组合模式组装一棵完整的节点树,即可代表一条动态 SQL 语句。但是内部具体如何去组装 SqlNode 节点树我没记住,如果之后有需要,我可以再来对应的模块找到相应的代码,并且有了这个思路之后,大模型也可以给出比较好的具体实现。

这是第一点收获:阅读源码,适当放弃对细节的记忆,而是看完代码之后,能提炼出源码针对 XX 问题,通过 XX 设计来解决

第二点收获是了解到了技术实力本质是”解决问题的能力“,而技术实力的强弱体现在对应问题的“复杂度”,复杂度越高,则代表能力越强,推荐一篇文章:当我们聊技术实力的时候,我们到底在聊什么

因此,准确识别问题的复杂度很重要,以 MyBatis 为例:

  • MyBatis 框架的核心任务是解析 SQL,并建立 SQL 与 Mapper 接口之间的关联,复杂度体现在:

    1. 动态 SQL 的解析涉及到了 SqlNode 节点树,通过组合模式进行组装。

    2. SQL 与 Mapper 接口的关联涉及到了代理模式,如何创建代理对象?SQL 与 Mapper 接口之间的对应关系如何?

  • MyBatis 框架提供了一级/二级缓存能力,缓存的复杂度体现在:

    1. 二级缓存能力的开/关控制如何实现?
    2. 缓存 Key 设计时需要考虑哪些因素?
  • 由于有分页的需要,MyBatis 内部如何实现了分页,分页的复杂度体现在:

    1. MyBatis 内部如何实现分页?
    2. 分页插件 PageHelper 内部又如何实现分页?
    3. MyBaitis 内部的分页和 PageHelper 的分页之间的区别是什么?

1 MyBatis 是怎么运行的?

在研究 MyBatis 源码之前,先通过一个 Demo 了解 MyBatis 操作数据库的运行流程,通过 Demo 需要了解:

  • 运行 MyBatis 都需要哪些类的配合?
  • 运行 MyBatis 需要哪些配置文件?

该 Demo 来源于 Github 开源项目:github.com/yeecode/MyB…

(1)MyBatis 初始化代码如下:

// 主启动类
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {

        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            // 1、读取 xml 配置文件
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 2、得到 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);

        // 3、创建 SqlSession
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 4、通过 SqlSession 获取对应 Mapper
            UserMapper userMapper = session.getMapper(UserMapper.class);
            
            User userParam = new User();
            userParam.setSchoolName("Sunny School");
            // 5、调用接口展开数据库操作
            List<User> userList =  userMapper.queryUserBySchoolName(userParam);
            
            for (User user : userList) {
                System.out.println("name : " + user.getName() + " ;  email : " + user.getEmail());
            }
        }
    }
}

初始化代码中主要包括 5 步骤:

1、读取 mybatis-config.xml 配置文件,得到对应的字节输入流 InputStream

2、创建 SqlSessionFactory:通过构建者模式创建,在 build() 方法中,会将 mybatis-config.xml 配置文件的字节流解析为配置对象。

3、创建 SqlSession:可以将 SqlSession 理解为 MyBatis 与数据库交互的通道,在 SqlSession 内部定义了数据库操作,可以看作是执行 SQL 的入口。

4、创建 UserMapper 代理对象:通过 SqlSessionUserMapper 接口创建代理对象,通过“动态代理”可以实现用户无感知的 SQL 操作。

5、调用 UserMapper 代理对象的方法,进行数据库操作。

(2)MyBatis 的配置文件

MyBatis 的配置文件为 mybatis-config.xml,内部指定了数据源、Mapper 文件地址等信息。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="com.github.yeecode.mybatisdemo"/>
    </typeAliases>

    <environments default="development">
        <environment id="development">
            <transactionManager type="JDBC"/>
                <dataSource type="POOLED">
                    <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
                    <property name="url" value="jdbc:mysql://127.0.0.1:3306/yeecode?serverTimezone=UTC"/>
                    <property name="username" value="root"/>
                    <property name="password" value="123456"/>
                </dataSource>
        </environment>
    </environments>

    <mappers>
        <mapper resource="com/github/yeecode/mybatisdemo/UserMapper.xml"/>
    </mappers>
</configuration>

(3)SQL 文件

除了配置文件,还需要在 Mapper.xml 文件内部定义 SQL 语句,通过 Mapper.xml 文件内部的 [namespace + id] 实现与 Mapper 接口的映射。

如下,namespace 即指定了 UserMapper 接口的权限定类名,在 <select> 标签内的 id 属性即指定了 UserMapper 接口对应的方法名:

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper   PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.github.yycome.UserTest.UserMapper">
    <select id="queryUserBySchoolName" resultType="com.github.yycome.UserTest.User">
        SELECT * FROM `user`
        <if test="schoolName != null">
            WHERE schoolName = #{schoolName}
        </if>
    </select>
</mapper>

接下来会对 MyBatis 初始化代码中的五个步骤分别进行介绍。

1.1 xml 配置文件读取

image-20250306131624073

本节将会介绍 MyBatis 初始化代码的「步骤 1」,即 MyBatis 内部如何读取 mybatis-config.xml 文件:

// 1、读取 xml 配置文件
String resource = "mybatis-config.xml";
inputStream = Resources.getResourceAsStream(resource);

在读取配置文件时,首先通过 Resources 类将 mybatis-config.xml 配置文件读取为 InputStream 字节输入流,后续基于字节流解析配置文件里的内容。

Resources 读取配置文件代码如下:

// MyBatis 源码内部 io 包下的 Resource 类 
public class Resources {
    // 方法 1:内部适配了「方法 2」的调用
    public static InputStream getResourceAsStream(String resource) throws IOException {
      return getResourceAsStream(null, resource);
    }

    // 方法 2
    public static InputStream getResourceAsStream(ClassLoader loader, String resource) throws IOException {
      InputStream in = classLoaderWrapper.getResourceAsStream(resource, loader);
      if (in == null) {
        throw new IOException("Could not find resource " + resource);
      }
      return in;
    }
}

适配器模式的使用:

Resources 读取资源时,使用了 适配器模式 ,如上,Resources 类有两个 getResourceAsStream() 方法。

其中 getResourceAsStrea m(ClassLoader loader, String resource) 方法有两个参数,但是有些调用方只传一个参数,另外一个参数没有传值。

借助于适配器模式,增加一个新的方法 getResourceAsStream(String resource),实现对未传参数的适配。

getResourceAsStream(String resource) 中,调用了带有两个参数的 getResourceAsStream(ClassLoader loader, String resource) 方法,并且将没有传入的 ClassLoader 参数设置为 null,以此适配调用方和被调用方参数不一致问题。

Resources 类内部,最终会通过“类加载器”读取对应的 mybatis-config.xml 文件,如下:

// MyBatis 源码内部 io 包下的 ClassLoaderWrapper 类
public class ClassLoaderWrapper {
    
    // 通过类加载器读取资源
    InputStream getResourceAsStream(String resource, ClassLoader[] classLoader) {
      // 1、遍历类加载器
      for (ClassLoader cl : classLoader) {
        if (null != cl) {
          // 2、使用类加载器 ClassLoader 读取 xml 配置文件为 InputStream 字节输入流
          InputStream returnValue = cl.getResourceAsStream(resource);
          // 3、一些类加载器需要前边加上 "/",尝试再读一次
          if (null == returnValue) {
            returnValue = cl.getResourceAsStream("/" + resource);
          }
          // 4、读取到则直接返回
          if (null != returnValue) {
            return returnValue;
          }
        }
      }
      return null;
    }
}

类加载器:

ClassLoaderWrapper#getResourceAsStrem() 方法的入参传入了 ClassLoaderp[] 类加载器数组,方法内部会遍历类加载器去读取 xml 配置文件。

ClassLoader 是 Java 语言自身的类加载器,作用是动态加载类(即 .class 文件),同样也可以将 xml 文件读取为输入流。

其中,入参的 ClassLoader[] 数组通过 getClassLoaders() 方法返回,如下:

// MyBatis 源码内部 io 包下的 ClassLoaderWrapper 类
public class ClassLoaderWrapper {
    ClassLoader[] getClassLoaders(ClassLoader classLoader) {
      return new ClassLoader[]{
          classLoader,
          defaultClassLoader,
          Thread.currentThread().getContextClassLoader(),
          getClass().getClassLoader(),
          systemClassLoader};
    }
}

为什么要通过多个 ClassLoader 加载资源呢?

不同类型的 ClassLoader 加载资源的位置也是不同的,通过多个 ClassLoader 可以确保在不同环境下都可以加载到对应的资源。

除此之外,从前向后遍历 ClassLoader 数组加载资源,只要成功加载就立马返回,因此数组中越靠前的 classLoader 加载资源优先级越高。

getClassLoaders() 方法参数传入的 ClassLoader 排在数组的第一个,因此它的优先级最高;systemClassLoader 在最后一个,对应的优先级最低。

ClassLoader 内部加载资源流程:

ClassLoader 类加载器对资源的加载涉及到“双亲委派机制”。

双亲委派机制:ClassLoader 在加载资源时,会将资源加载的请求委托给父类加载器去完成,如果父类加载器也存在其父类加载器,则继续向上委托。如果父类可以完成资源的加载,就成功返回;如果父类加载器未完成资源的加载,则会由子类加载器去尝试加载。

ClassLoader 中,获取资源的代码如下:

public abstract class ClassLoader {
    private final ClassLoader parent;

    public URL getResource(String name) {
        URL url;
        // 1、寻找父类加载器
        if (parent != null) {
            url = parent.getResource(name);
        } else {
            // 2、Java 中引导类加载器为 null,因此这里表示已使用引导类加载器去加载资源
            url = getBootstrapResource(name);
        }
        // 3、父类加载器未加载资源,则自己进行加载
        if (url == null) {
            url = findResource(name);
        }
        return url;
    }
}

getResource() 加载资源步骤如下:

「步骤 1」发现父类加载器不为 null,则请求父类加载器完成资源加载。

「步骤 2」如果发现 parent == null,则说明父类加载器为引导类加载器,直接使用引导类加载器加载对应资源。

「步骤 3」会判断父类加载器是否成功加载资源,如果没有,则自己再尝试加载资源。

1.2 xml 配置文件解析

本节介绍流程如下图灰色标记:

image-20250306131713236

在 1.1 节中,将 mybatis-config.xml 配置文件读取为字节流之后,之后还需要将字节流解析为“配置数据”供运行时使用,本节将会介绍将“字节流”解析为 Configuration 配置类的过程。

读完本节,你将会学到:

  • SqlSessionFacatorySqlSession 创建过程中涉及的设计模式,以及两种设计模式之间的区别。

  • SqlSession 在 MyBatis 内部扮演了什么角色?

在创建 SqlSessionFactory 时,就会解析 xml 配置文件对应的字节输入流,之后通过 SqlSessionFactory 去创建 SqlSession 对象。

其中,SqlSessionFactorySqlSession 都是 MyBatis 内部的核心接口。

1.2.1 SqlSessionFactory

为了方便阅读,重复贴一下 MyBatis 的初始化代码:

// 2、得到 SqlSessionFactory
SqlSessionFactory sqlSessionFactory =
	new SqlSessionFactoryBuilder().build(inputStream);

// 3、创建 SqlSession
try (SqlSession session = sqlSessionFactory.openSession()) {}

在「步骤 2」中,会创建 SqlSessionFactoryBuilder,作为 SqlSessionFactory 的构造器,通过它的 build() 方法去构造 SqlSessionFactory,涉及到 构建者模式

在「步骤 3」通过 SqlSessionFactory 创建 SqlSession 就利用了 工厂模式

这里同时涉及到了 构建者模式工厂模式

工厂模式和构建者模式的区别:

  • 工厂模式 更关注如何创建对象。通过 工厂模式 可以将对象的构建过程隐藏起来,调用者并不关心内部属性的赋值,直接通过工厂即可获取对象示例。

  • 构建者模式 更关注创建对象的内部细节。通过 构建者模式 可以将复杂的对象构建过程剥离出来,调用者可以指定对应属性,并且构建对象。

「步骤 2」在通过 SqlSessionFactoryBuilder 创建 SqlSessionFactory 时,在构建者模式中,调用者会传入一些参数来构建对象,调用者可以根据需要传入不同的参数。

「步骤 3」在调用 openSession() 方法时没有传入参数,因为在工厂模式中,调用者不关心内部创建细节。

SqlSessionFactoryBuilder 中,同样也用了 适配器模式,如下前三个 build() 方法最后都是调用了最后一个 build() 方法,在不同的 build() 方法之间做了参数的适配,如下图:

image-20240921160726375

解析 mybatis-config.xml 配置文件字节流的代码位于 build() 方法内部,如下:

// MyBatis 源码 session 包下的 SqlSessionFactoryBuilder 类
public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
      try {
        // 1、创建解析器
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // 2、执行解析方法
        return build(parser.parse());
      }
      // ...
    }
}

build() 方法内部,又进一步将解析代码封装在了 XMLConfigBuilder 中,从而类的功能更加细分。

build() 方法内部主要有两个步骤:

  • parser.parse() :将 InputStream 解析为 Configuration。也就是将 MyBatisxml 配置文件中的内容都解析到了 Configuration 类中进行存储。
  • build():根据解析好的 Configuration 配置类,创建 SqlSessionFactory 对象。

变量命名技巧:

XMLConfigBuilder 的职责是解析输入字节流,因此将其命名为 parser,好处是根据变量名就可以猜测出来对应的职责,代码可读性大大增强。

XMLConfigBuilderparse() 解析如下:

// MyBatis 源码 session 包下的 XMLConfigBuilder 类
public class XMLConfigBuilder extends BaseBuilder {
    public Configuration parse() {
      // 1、解析 xml 配置文件根标签 <configuration> 里的内容
      parseConfiguration(parser.evalNode("/configuration"));
      return configuration;
    }
}

「步骤 1」会根据标签对 mybatis-config.xml 的内容进行解析。

mybatis-config.xml 配置文件的根标签为 <configuration> ,因此传入了 /configuration 表示解析根标签内部的内容,如下。

image-20240921203152909

xml 配置文件中解析到的内容,会通过 Configuration 配置类存储,在创建 SqlSessionFactory 对象时,就会传入创建的 Configuration 配置类:

// MyBatis 源码 session 包下的 SqlSessionFactoryBuilder 类
public class SqlSessionFactoryBuilder {
    public SqlSessionFactory build(Configuration config) {
      return new DefaultSqlSessionFactory(config);
    }
}

至此,就完成了 xml 配置文件的解析以及 SqlSessionFactory 工厂的创建。

1.2.2 SqlSession

SqlSession 为 MyBatis 内部核心接口,接口内定义了“增删改查”的方法,因此 SqlSession 作为入口,负责 SQL 语句的执行,接口定义如下:

public interface SqlSession extends Closeable {
  <T> T selectOne(String statement);
  <T> T selectOne(String statement, Object parameter);
  <E> List<E> selectList(String statement);
  <E> List<E> selectList(String statement, Object parameter);
  int insert(String statement);
  int insert(String statement, Object parameter);
  int update(String statement);
  int update(String statement, Object parameter);
  int delete(String statement);
  int delete(String statement, Object parameter);
  // ... 只展示部分方法
}

DefaultSqlSessionSqlSession 接口的默认实现类,创建流程如下:

public class DefaultSqlSessionFactory implements SqlSessionFactory {
	// 创建 DefaultSqlSession
    private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
      Transaction tx = null;
      try {
        // 1、获取 xml 配置文件中的 <environment> 标签中的数据源信息
        final Environment environment = configuration.getEnvironment();
        // 2、创建事务管理工厂
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        // 3、创建事务管理器
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // 4、创建 Executor 实例
        final Executor executor = configuration.newExecutor(tx, execType);
        // 5、创建 DefaultSqlSession
        return new DefaultSqlSession(configuration, executor, autoCommit);
      } 
      // ...
    }
}

DefaultSqlSession 内部主要有两个关键属性:Configuration(配置类)和 Executor(执行器):

  • Configuration:存储 MyBatis 的配置属性,如数据源信息、解析的 SQL 语句等。
  • Executor:封装 SQL 语句的执行逻辑,SqlSession 内部 SQL 语句的执行最终会委托给 Executor 来完成。

1.3 SQL 执行的关键:代理模式

本节介绍流程如下图灰色标记:

image-20250318141918944

创建 DefaultSqlSession 之后,可以通过 SqlSession 对象获取 UserMapper 接口的代理对象。

之后,当调用 UserMapper 中的方法时,就会走到 代理的拦截器 中,在拦截器内部会执行数据库相关的操作。

代理模式的使用场景: 将通用且复杂的操作,放到代理的拦截器中隐藏起来。

例如 RPC 远程调用,我们希望对远程方法的调用和本地方法调用一样简单,那么就在本地创建一个与远程方法一样的接口,并创建该接口的代理对象。当调用接口内部的方法时,走到代理的 拦截器 中,在拦截器内部完成寻找远程服务的 ip + 方法,发起远程调用,并拿到调用结果返回。

接下来会介绍如何通过 SqlSession 获取 UserMapper 代理对象,重复贴一下 MyBatis 运行 Demo:

// 主启动类
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {

        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            // 1、读取 xml 配置文件
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 2、得到 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);

        // 3、创建 SqlSession
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 4、通过 SqlSession 获取对应 Mapper
            UserMapper userMapper = session.getMapper(UserMapper.class);
            
            User userParam = new User();
            userParam.setSchoolName("Sunny School");
            // 5、调用接口展开数据库操作
            List<User> userList =  userMapper.queryUserBySchoolName(userParam);
            
            for (User user : userList) {
                System.out.println("name : " + user.getName() + " ;  email : " + user.getEmail());
            }
        }
    }
}

「步骤 4」通过 DefaultSqlSessiongetMapper() 获取 UserMapper 接口的代理对象,如下:

// MyBatis 源码 session 包下的 DefaultSqlSession 类
public class DefaultSqlSession implements SqlSession {
    public <T> T getMapper(Class<T> type) {
      return configuration.getMapper(type, this);
    }
}

DefaultSqlSession 内部,通过 Configuration 创建代理对象,getMapper() 方法如下:

// MyBatis 源码 session 包下的 Configuration 类
public class Configuration {
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      return mapperRegistry.getMapper(type, sqlSession);
    }
}

为了类的单一职责,在 Configuration 类内部又通过 MapperRegistry 来创建对应的代理对象。如下:

// MyBatis 源码 binding 包下的 MapperRegistry 类
public class MapperRegistry {
    // 用于缓存 Mapper 接口对应的 MapperProxyFactory 对象
    private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();
    // 获取 Mapper 接口的代理对象
    public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
      // 1、先获取 MapperProxyFactory
      final MapperProxyFactory<T> mapperProxyFactory = (MapperProxyFactory<T>) knownMappers.get(type);
      try {
        // 2、通过工厂创建 Mapper 对象
        return mapperProxyFactory.newInstance(sqlSession);
      } catch (Exception e) {
        throw new BindingException("Error getting mapper instance. Cause: " + e, e);
      }
    }
}

MapperRegistry 通过 Map 存储每个 Mapper 接口对应的 MapperProxyFactory 对象,也就是代理对象的工厂,当真正需要获取代理对象时,通过 MapperProxyFactory#newInstance 方法创建对应的代理对象。

「步骤 1」会先尝试在缓存 knowMappers 中获取代理对象对应的工厂,「步骤 2」则会通过获取 MapperProxyFactory 工厂去创建代理对象。

「步骤 2」的 newInstance() 方法如下:

// MyBatis 源码 binding 包下的 MapperProxyFactory 类
public class MapperProxyFactory<T> {
    public T newInstance(SqlSession sqlSession) {
      // 1、创建 MapperProxy
      final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
      // 2、调用内部方法
      return newInstance(mapperProxy);
    }

    protected T newInstance(MapperProxy<T> mapperProxy) {
      // 3、创建代理对象
      return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
    }
}

「步骤 1」创建了 MapperProxy 对象,该对象就是代理的拦截器,MapperProxy 是执行 SQL 语句的关键。

「步骤 3」通过 Proxy.newProxyInstance() 创建 JDK 动态代理对象。

之后,当调用 UserMapper 代理对象的方法时,会走到拦截器 MapperProxy 中,在拦截器中,会执行 xml 中的 SQL 语句,并返回语句执行结果。

因此在 MyBatis 中,MapperProxy 是将 UserMapper 接口和数据库操作连接起来的关键。

这里有个问题可以思考一下:

  • 代理有两种:JDK 动态代理和 CGLIB 动态代理,这里为什么使用 JDK 动态代理?

这里使用 JDK 动态代理 是因为 JDK 动态代理的定位就是对「接口」进行代理,原理是创建被代理接口的实现类,以此对接口中的方法进行增强。

CGLIB 动态代理 的定位是对「普通类」进行代理,原理是生成被代理类的子类。

在 MyBatis 中要求 Mapper 都是接口,因此从定位上来看,使用 JDK 动态代理是比较合适的。

1.4 代理拦截器

本节介绍流程如下图灰色标记:

image-20250318144234044

重复贴一下 MyBatis 运行 Demo:

// 主启动类
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {

        String resource = "mybatis-config.xml";
        InputStream inputStream = null;
        try {
            // 1、读取 xml 配置文件
            inputStream = Resources.getResourceAsStream(resource);
        } catch (IOException e) {
            e.printStackTrace();
        }
        // 2、得到 SqlSessionFactory
        SqlSessionFactory sqlSessionFactory =
                new SqlSessionFactoryBuilder().build(inputStream);

        // 3、创建 SqlSession
        try (SqlSession session = sqlSessionFactory.openSession()) {
            // 4、通过 SqlSession 获取对应 Mapper
            UserMapper userMapper = session.getMapper(UserMapper.class);
            
            User userParam = new User();
            userParam.setSchoolName("Sunny School");
            // 5、调用接口展开数据库操作
            List<User> userList =  userMapper.queryUserBySchoolName(userParam);
            
            for (User user : userList) {
                System.out.println("name : " + user.getName() + " ;  email : " + user.getEmail());
            }
        }
    }
}

在「步骤 5 」中,调用的 UserMapper 其实就已经是生成的代理对象了。

因此在「步骤 5 」中调用 UserMapper 方法时,会走到代理对象的拦截器 MapperProxy 中,进行数据库相关的处理。

MapperProxyinvoke() 方法内部为代理拦截的逻辑,如下:

// MyBatis 源码 binding 包下的 MapperProxy 类
public class MapperProxy<T> implements InvocationHandler, Serializable {
    // 拦截方法
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      try {
        // 1、如果调用的 Object 的方法:equals()、toString()、hashCode() 等,则不进行代理
        if (Object.class.equals(method.getDeclaringClass())) {
          return method.invoke(this, args);
        } else if (method.isDefault()) {
          return invokeDefaultMethod(proxy, method, args);
        }
      } catch (Throwable t) {
        throw ExceptionUtil.unwrapThrowable(t);
      }
      final MapperMethod mapperMethod = cachedMapperMethod(method);
      // 2、核心方法
      return mapperMethod.execute(sqlSession, args);
    }
}

「步骤 1」会判断执行的方法是否为 Mapper 接口的方法,如果是 equals()toString() 等与数据库操作无关的方法,则不需要拦截。

「步骤 2」专门封装了 MapperMethod 类去执行对应的 SQL 语句,进入内部的 execute() 方法:

// MyBatis 源码 binding 包下的 MapperMethod 类
public class MapperMethod {
    public Object execute(SqlSession sqlSession, Object[] args) {
      Object result;
      switch (command.getType()) {
        case INSERT: {
          // ... 
        }
        case UPDATE: {
          // ...
        }
        case DELETE: {
          // ...
        }
        case SELECT:
          // ...
          } else if (method.returnsMany()) {
            // 这里查询的是 List 集合,因此会走到下边这行  
            result = executeForMany(sqlSession, args);
          }
          // ...
      }
      return result;
    }
}

由于调用的是 UserMapper 的查询方法,返回值是 List 集合,经过条件判断,最终会走到 executeForMany() 方法内部。

executeForMany() 方法中,会调用 SqlSession 中定义的增删改查方法。如下:

// MyBatis 源码 binding 包下的 MapperMethod 类
public class MapperMethod {
    private <E> Object executeForMany(SqlSession sqlSession, Object[] args) {
      // 1、查询参数的转换
      Object param = method.convertArgsToSqlCommandParam(args);
      if (method.hasRowBounds()) {
        // ...
      } else {
        // 2、执行 SQL 查询
        result = sqlSession.selectList(command.getName(), param);
      }
      // ...
      return result;
    }
}

「步骤 1」会先进行查询参数的转换,这里先不细看内部流程。

将注意力放在「步骤 2」查询方法 selectList() ,如下:

// MyBatis 源码 session 包下的 DefaultSqlSession 类
public class DefaultSqlSession implements SqlSession {
    public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
      try {
        // 1、先拿到 MappedStatement  
        MappedStatement ms = configuration.getMappedStatement(statement);
        // 2、通过执行器去完成查询  
        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();
      }
    }
}

在执行 SQL 查询之前,「步骤 1」需要先获取 SQL 语句、参数映射、结果映射等信息,这些信息存储在 MappedStatement 内部。

MappedStatement 存储在 Configuration 内部的 Map 集合,以 statementkey 获取。

MappedStatementSQL 语句之间是一一对应的关系,比如 UseerMapper 内部的 queryUserBySchoolName 方法就对应一个 MappedStatement

Map 中的 key,即 statement 就是调用的 UserMapper 接口的「全限定类名」 +「 方法名」。如下图:

image-20240922135210746

在「步骤 2」中,SQL 的执行最终交给了执行器 Executor

执行器 Executor 有很多类型,涉及到 装饰器模式CachingExecutor 通过对其他几种 Executor 进行包装增强,提供了二级缓存的功能。

二级缓存的实现细节在之后会介绍,这里先从整体对 MyBatis 的执行流程有一个大致的了解。

1.5 总结

至此,MyBatis 整体的运行流程就介绍完成了,对于每个功能点并没有详细地介绍底层具体如何实现,而是先从整体上介绍 MyBatis 内部执行 SQL 语句的流程,以及涉及到哪些类,每个类的职责是怎样的。

在 MyBatis 内部,对象的创建基本都采用工厂模式和构建者模式来完成,大量采用了设计模式;并且每个方法基本上都很短,主干流程清晰,每个类的职责单一,变量的命名灵活清晰,通过类名、变量名可以快速了解该类的职责,提升代码可读性。

在 SQL 执行中,将 SQL 语句与 Mapper 接口连接起来的关键就是 MapperProxy,通过代理模式将复杂的数据库操作封装起来。

通过对 Mapper 接口创建代理对象,从而使 Mapper 接口内部方法的执行被 MapperProxy 所拦截,在 MapperProxy 中会去完成 SQL 语句的执行。