目录

MyBatis源码分析配置文件解析

MyBatis源码分析の配置文件解析


前言

本篇主要介绍MyBatis源码中的 配置文件解析 部分。MyBatis是对于传统JDBC的封装,屏蔽了传统JDBC与数据库进行交互,组装参数,获取查询结果并自己封装成对象的繁琐过程。

原生MyBatis首先需要配置 mybatis-config.xml

<?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>
    <properties resource="jdbc.properties"/>
    <environments default="dev">
        <environment id="dev">
            <transactionManager type="JDBC"/>
            <dataSource type="POOLED">
                <property name="driver" value="${jdbc.driver}"/>
                <property name="url" value="${jdbc.url}"/>
                <property name="username" value="${jdbc.username}"/>
                <property name="password" value="${jdbc.password}"/>
            </dataSource>
        </environment>
    </environments>
    <mappers>
        <mapper resource="mapper/UserMapper.xml"/>
    </mappers>
</configuration>

并且指定数据源 jdbc.properties

jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/test?useSSL=false&serverTimezone=UTC
jdbc.username=root
jdbc.password=123456

创建数据库访问层接口:

public interface UserMapper {
    List<User> selectAll();

    User selectById(int id);

    void insert(User user);

    void update(User user);

    void delete(int id);
}

以及对应的xml文件:

<?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.example.mybatis.mapper.UserMapper">
    <cache/>
    <resultMap id="userResultMap" type="com.example.mybatis.entity.User">
        <id property="id" column="id"/>
        <result property="name" column="name"/>
        <result property="age" column="age"/>
    </resultMap>

    <select id="selectAll" resultMap="userResultMap">
        SELECT * FROM users
    </select>

    <select id="selectById" resultMap="userResultMap" parameterType="int">
        SELECT * FROM users WHERE id = #{id}
    </select>

    <insert id="insert" parameterType="com.example.mybatis.entity.User">
        INSERT INTO users (name, age) VALUES (#{name}, #{age})
    </insert>

    <update id="update" parameterType="com.example.mybatis.entity.User">
        UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}
    </update>

    <delete id="delete" parameterType="int">
        DELETE FROM users WHERE id = #{id}
    </delete>
</mapper>

mybatis-config.xml 常见的标签:

标签作用
<settings>控制 MyBatis 全局行为(缓存、懒加载、日志等)
<typeAliases>设置类型别名,简化 Mapper XML 中类名书写
<typeHandlers>自定义类型转换器(Java类型 ↔ JDBC类型)
<plugins>注册插件(如分页插件、SQL打印等)
<objectFactory>自定义对象创建逻辑
<environments>配置数据库环境及事务管理
<mappers>注册 Mapper 映射文件或 Mapper 接口

原生MyBatis的使用,其中读取配置文件并进行解析,主要体现在 SqlSessionFactoryBuilderbuild 方法中:

public class Main {
    public static void main(String[] args) throws Exception {
        //将xml构筑成configuration配置类
        Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
        //解析xml,注册成SqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);

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

            User user = session.selectOne("com.example.mybatis.mapper.UserMapper.selectById", 1);

            System.out.println(user);
        }
    }
}

一、SqlSessionFactoryBuilder

1.1、XMLConfigBuilder

在调用 SqlSessionFactoryBuilderbuild 方法时,首先会去创建一个 XMLConfigBuilder ,目的是构建一个XML配置文件解析器对象。

https://i-blog.csdnimg.cn/direct/0594157ddf914c64a3241e4279993588.png 其中的核心代码,这段代码的作用是注册别名,将配置文件中的 “JDBC”、“POOLED"等关键词和实际的类型进行绑定。

https://i-blog.csdnimg.cn/direct/7a2ee64c29c045dbab46f8b44bd0dc6a.png

别名实际类用途
"JDBC"JdbcTransactionFactoryJDBC事务管理器(默认事务方式)
"MANAGED"ManagedTransactionFactory受容器管理的事务(如 Spring)
"JNDI"JndiDataSourceFactory从 JNDI 获取数据源
"POOLED"PooledDataSourceFactory数据库连接池(MyBatis 内置)
"UNPOOLED"UnpooledDataSourceFactory不使用连接池的数据源
"PERPETUAL"PerpetualCache永久缓存
"FIFO"FifoCache先进先出缓存
"LRU"LruCache最近最少使用缓存
"SOFT"SoftCache基于 SoftReference 的缓存
"WEAK"WeakCache基于 WeakReference 的缓存
"DB_VENDOR"VendorDatabaseIdProvider根据数据库类型自动切换 SQL
"XML"XMLLanguageDriverMyBatis 默认的 XML SQL 语言驱动器
"RAW"RawLanguageDriver原生 SQL 写法语言驱动器
"SLF4J"Slf4jImpl使用 SLF4J 的日志输出
"COMMONS_LOGGING"JakartaCommonsLoggingImpl使用 Commons Logging 日志
"LOG4J"Log4jImpl使用 Log4j 日志
"LOG4J2"Log4j2Impl使用 Log4j2 日志
"JDK_LOGGING"Jdk14LoggingImpl使用 JDK 内建日志
"STDOUT_LOGGING"StdOutImpl输出日志到控制台
"NO_LOGGING"NoLoggingImpl不输出日志
"CGLIB"CglibProxyFactory使用 CGLIB 动态代理
"JAVASSIST"JavassistProxyFactory使用 Javassist 动态代理

1.2、parse

真正解析配置文件的是利用上一步构造出的 XMLConfigBuilderparse 方法,首先会进行判断,如果已经解析过,则抛出异常,不会重复解析:

https://i-blog.csdnimg.cn/direct/0fb6d72be4ea4d5fa588029868dd80b7.png 否则就将标记设置为true。并且执行 parseConfiguration 方法,从根节点进行解析:

每一行都对应了一个 <mybatis-config.xml> 中的标签,逐步填充 Configuration 对象内容:

https://i-blog.csdnimg.cn/direct/37e0f047cfd14217a3ac8c65f35d5c56.png

/**
 * 解析 <configuration> 根节点的各个子标签,并将配置信息填充到 Configuration 对象中
 */
private void parseConfiguration(XNode root) {
  try {
    // 【1】先解析 <properties> 标签(必须最优先解析),以便后续标签中的占位符 ${} 能被正确替换
    propertiesElement(root.evalNode("properties"));

    // 【2】解析 <settings> 标签,将其内容转换为 Properties 对象
    Properties settings = settingsAsProperties(root.evalNode("settings"));

    // 【3】解析 settings 中的 vfsImpl 属性(如果配置了自定义 VFS 实现类)
    loadCustomVfs(settings);

    // 【4】解析 settings 中的 logImpl 属性(设置日志实现类,如 LOG4J、STDOUT_LOGGING 等)
    loadCustomLogImpl(settings);

    // 【5】解析 <typeAliases> 标签,注册用户自定义的别名或包扫描别名
    typeAliasesElement(root.evalNode("typeAliases"));

    // 【6】解析 <plugins> 标签,注册 MyBatis 插件(如分页插件、SQL 拦截器等)
    pluginElement(root.evalNode("plugins"));

    // 【7】解析 <objectFactory> 标签,设置自定义对象工厂(用于实例化结果对象)
    objectFactoryElement(root.evalNode("objectFactory"));

    // 【8】解析 <objectWrapperFactory> 标签,自定义对象包装器(封装结果对象属性访问行为)
    objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));

    // 【9】解析 <reflectorFactory> 标签,自定义反射器工厂(高级反射行为控制)
    reflectorFactoryElement(root.evalNode("reflectorFactory"));

    // 【10】将 <settings> 中的配置项应用到 Configuration 对象中
    settingsElement(settings);

    // 【11】解析 <environments> 标签,注册事务管理器和数据源配置(必须在 objectFactory 之后执行)
    environmentsElement(root.evalNode("environments"));

    // 【12】解析 <databaseIdProvider> 标签,支持数据库厂商识别(如区分 MySQL、Oracle)
    databaseIdProviderElement(root.evalNode("databaseIdProvider"));

    // 【13】解析 <typeHandlers> 标签,注册自定义类型处理器(TypeHandler)
    typeHandlerElement(root.evalNode("typeHandlers"));

    // 【14】解析 <mappers> 标签,加载 Mapper 映射器(包括 XML 和接口方式)
    mapperElement(root.evalNode("mappers"));

  } catch (Exception e) {
    // 如果解析过程中发生异常,则封装为 BuilderException 抛出
    throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
  }
}

当解析完成后,会得到一个 configuration 对象,其中就包含了配置文件中的各种值。相当于此时的xml配置文件已经转化为了 configuration 对象。最后还会将其再次包装成 SqlSessionFactory ,后续会利用 SqlSessionFactory 进行sql相关逻辑的执行。

https://i-blog.csdnimg.cn/direct/d676ae7afe224c4c8dfbfcc6dbe264f7.png 其中最关键的是mappers标签的解析。

二、mappers标签的解析

mapperElement 方法,首先会拿到 mappers 根标签,然后进行解析。

/**
 * 解析 <mappers> 标签,支持三种加载方式:package、resource/url、class
 */
private void mapperElement(XNode parent) throws Exception {
  if (parent != null) {
    // 遍历 <mappers> 下的所有子节点(可能是 <package> 或 <mapper>)
    for (XNode child : parent.getChildren()) {

      // 情况1:<package name="com.xxx.mapper"/>,批量注册包下所有 Mapper 接口
      if ("package".equals(child.getName())) {
        String mapperPackage = child.getStringAttribute("name");
        // 自动扫描指定包下的所有接口,并注册到 Configuration 中
        configuration.addMappers(mapperPackage);

      } else {
        // 情况2~4:单个 <mapper> 节点,通过 resource/url/class 指定加载方式
        String resource = child.getStringAttribute("resource"); // 从 classpath 中加载 Mapper XML
        String url = child.getStringAttribute("url");           // 从网络路径加载 Mapper XML
        String mapperClass = child.getStringAttribute("class"); // 直接加载 Mapper 接口类

        // 情况2:只指定 resource,加载 Mapper XML 文件
        if (resource != null && url == null && mapperClass == null) {
          ErrorContext.instance().resource(resource); // 设置错误上下文信息
          try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(
              inputStream, configuration, resource, configuration.getSqlFragments());
            mapperParser.parse(); // 解析 Mapper XML,注册语句映射
          }

        // 情况3:只指定 url,加载远程 Mapper XML 文件
        } else if (resource == null && url != null && mapperClass == null) {
          ErrorContext.instance().resource(url);
          try (InputStream inputStream = Resources.getUrlAsStream(url)) {
            XMLMapperBuilder mapperParser = new XMLMapperBuilder(
              inputStream, configuration, url, configuration.getSqlFragments());
            mapperParser.parse(); // 同样调用解析逻辑
          }

        // 情况4:只指定 class,注册 Mapper 接口类(无 XML 时适用)
        } else if (resource == null && url == null && mapperClass != null) {
          Class<?> mapperInterface = Resources.classForName(mapperClass);
          configuration.addMapper(mapperInterface); // 注册接口类到 MapperRegistry

        // 情况5:配置冲突,三种方式只能选一种,否则抛异常
        } else {
          throw new BuilderException(
            "A mapper element may only specify a url, resource or class, but not more than one.");
        }
      }
    }
  }
}

案例中对应的是 情况2 ,首先会注册一个mapper解析器,然后调用其parse方法对案例中 UserMapper.xml 进行解析,在该方法中,首先会进行判断,如果已经进行过解析,则不会重复解析。

https://i-blog.csdnimg.cn/direct/a718eb02f582466d8732bbfa3c4e2871.png 解析的核心方法在于 configurationElement ,同样是对于xml中的各种标签再次分类解析:

https://i-blog.csdnimg.cn/direct/e1f75ada3c384747a6d152ff76974576.png 这里重点看一下 cacheElement 以及 buildStatementFromContext

2.1、cacheElement

cacheElement 和Mybatis的二级缓存有关。简单的说,Mybatis有两级缓存:

  • 一级缓存是SqlSession 级别的,并且默认开启。
  • 二级缓存是Mapper 映射级别,默认不开启,如果需要,应该在某个mapper.xml中使用cache标签开启。

cacheElement 方法正是解析mapper.xml中的cache标签:

/**
 * 解析 <cache> 标签,构建二级缓存对象并注册到 Configuration 中。
 */
private void cacheElement(XNode context) {
  // 1. 判断 <cache> 标签是否存在
  if (context != null) {

    // 2. 解析缓存类型(默认是 PERPETUAL,即 PerpetualCache)
    String type = context.getStringAttribute("type", "PERPETUAL");
    Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);

    // 3. 解析缓存淘汰策略(默认是 LRU,即最近最少使用)
    String eviction = context.getStringAttribute("eviction", "LRU");
    Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);

    // 4. 缓存刷新间隔(可选):指定自动清空缓存的时间(毫秒)
    Long flushInterval = context.getLongAttribute("flushInterval");

    // 5. 缓存大小(可选):最大缓存对象个数
    Integer size = context.getIntAttribute("size");

    // 6. 是否为读写缓存(readOnly=false 表示使用序列化;true 表示共享引用)
    //    readWrite = true 表示开启对象副本,确保线程安全
    boolean readWrite = !context.getBooleanAttribute("readOnly", false);

    // 7. 是否阻塞:当缓存正在被其他线程刷新时,是否阻塞等待
    boolean blocking = context.getBooleanAttribute("blocking", false);

    // 8. 获取 <cache> 中配置的其他 <property> 子节点
    Properties props = context.getChildrenAsProperties();

    // 9. 构建缓存并注册到 Configuration,封装为 MapperBuilderAssistant.useNewCache()
    builderAssistant.useNewCache(
      typeClass,          // 缓存类型类(如 PerpetualCache)
      evictionClass,      // 淘汰策略类(如 LruCache)
      flushInterval,      // 缓存刷新间隔
      size,               // 缓存容量
      readWrite,          // 是否使用读写模式
      blocking,           // 是否阻塞模式
      props               // 自定义属性
    );
  }
}

useNewCache 中,最终会调用 CacheBuilderbuild 方法:

https://i-blog.csdnimg.cn/direct/fd0b65e3b72a43db8e31b88381d0d8c6.png build 方法中运用到了 装饰器模式 ,所有的Cache都实现了一个共同的父类Cache。

在**cache = newCacheDecoratorInstance(decorator, cache); **这一行代码中,传入 LruCache 和当前的 Cache 实例(PERPETUAL),将 PERPETUAL 包装到 LRU 中:(LruCache的delegate属性,指向的是传入的PerpetualCache实例)

https://i-blog.csdnimg.cn/direct/694a1f5b38fb452590f7b8ba5f6b3993.png** cache = setStandardDecorators(cache);**这一行代码,会继续进行装饰器的包装:

https://i-blog.csdnimg.cn/direct/86407e6568bd4d15985612eb626a2377.png setStandardDecorators 方法,对于 Cache 实例层层包装,赋值给各自的 delegate 属性:

https://i-blog.csdnimg.cn/direct/05e4d1116c304efb82824b27da99fceb.png 包装完成的层次:SynchronizedCache线程同步缓存区->LoggingCache统计命中率以及打印日志->SerializedCache序列化->LruCache最少使用->PerpetualCache默认。

https://i-blog.csdnimg.cn/direct/4e66792ea63c496a96a38423554e83d0.png

2.1.1、缓存策略

默认的 PerpetualCache ,使用的是 HashMap 进行存储。

https://i-blog.csdnimg.cn/direct/f7dc379c8b194bffb0c31472ab4a41ba.png

LruCache ,为了实现最近最少使用的机制,使用了 LinkedHashMap 的数据结构,并且重写了它的 removeEldestEntry 方法,关键在于, LinkedHashMap 构造时第三个参数为 true 表示按访问顺序排列:

https://i-blog.csdnimg.cn/direct/1d3f458ef17e4cb1b5c1d4f7d4675921.png

LruCache cache = new LruCache(new PerpetualCache("myCache"));
cache.setSize(3);

cache.put("A", 1);  // A
cache.put("B", 2);  // A B
cache.put("C", 3);  // A B C
cache.get("A");     // B C A (A 被访问过,移到尾部)
cache.put("D", 4);  // C A DB 被淘汰

SynchronizedCache ,每个方法上通过加 synchronized 保证线程安全:

https://i-blog.csdnimg.cn/direct/6afc7188cdff4175a2e7ab8fce3a589b.png LoggingCache ,会记录日志,以及统计缓存命中次数:

https://i-blog.csdnimg.cn/direct/1eaa2dddf434422db8fef6776a9514c9.png

2.2、buildStatementFromContext

buildStatementFromContext 是用来解析 select、insert、update、delete 标签中sql语句的方法,首先会解析出这些节点,然后进行循环,获取到 XMLStatementBuilder 后,执行 parseStatementNode 方法:

https://i-blog.csdnimg.cn/direct/07ef6424892142519b37a47f520c8ab4.pngparseStatementNode 方法中有几个关键点,这一段代码会判断当前的标签是否为select,如果是select标签, 则不会清除一级缓存(增删改会清除),以及判断是否使用二级缓存(默认 select 使用)

https://i-blog.csdnimg.cn/direct/ed315f0286c94a4cae15a0b3b28d1e4d.png

2.2.1、sql的解析

真正执行解析sql的是下图中的代码:

https://i-blog.csdnimg.cn/direct/b90dc84b08b348afb9c2da18cb423897.png 同样地会先去构建一个 XMLScriptBuilder ,然后调用其 parseScriptNode 方法进行解析:

https://i-blog.csdnimg.cn/direct/a8931f1133ee416b92be9682d15b49b0.pngparseScriptNode 方法中,首先会解析 SQL 标签中的所有子标签,然后去进行判断:

  • 包含动态 SQL(即是否包含 if、choose、${} 等动态节点)构建 DynamicSqlSource(运行时动态拼接 SQL)
  • 不包含动态 SQL(即是否包含 if、choose、${} 等动态节点)构建 RawSqlSource(直接编译成静态 SQL,提升效率)

https://i-blog.csdnimg.cn/direct/2aa6ae20cf6e4f7094c830fa3e54bb7e.png MixedSqlNode 对象,实现了 SqlNode 接口,SqlNode是所有动态 SQL节点的统一接口,而 MixedSqlNode 代表了 一整个 SQL 脚本块,比如select标签中所有内容就会变成一个 MixedSqlNode。

SqlNode 接口

├── MixedSqlNode // 组合节点

├── StaticTextSqlNode // 静态文本节点:普通 SQL 字符串

├── TextSqlNode // 动态文本节点:包含 ${}

├── IfSqlNode // if 标签

├── ChooseSqlNode // choose/when/otherwise

├── ForEachSqlNode // foreach

├── WhereSqlNode // where

├── TrimSqlNode // trim

├── SetSqlNode // set

└── BindSqlNode // bind

用一个案例说明,假如我在mapper.xml中定义了如下的sql语句:

<select id="findUser" parameterType="map" resultType="User">
  SELECT * FROM user
  <where>
    <if test="name != null">
      AND name = #{name}
    </if>
    <if test="age != null">
      AND age = #{age}
    </if>
  </where>
</select>

则生成的结构如下:

MixedSqlNode

├── StaticTextSqlNode(“SELECT * FROM user”)

└── WhereSqlNode

└── MixedSqlNode

├── IfSqlNode(test=“name != null”) → TextSqlNode(“AND name = #{name}”)

└── IfSqlNode(test=“age != null”) → TextSqlNode(“AND age = #{age}”)