源码白话说之 mybatis configuration 核心构建流程

517 阅读8分钟

configuration 配置类就是 mybatis 的核心大管家, mybatis 核心的配置信息基本上都存放在这里

01、Configuration 创建

在之前解析 SqlSessionFactory 的时候, build() 实现并没有详细解读, 而今天源码解析的主人公 Configuration 就在此方法中被初始化

这里以字节流的 build() 重载方法构建举例

public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
    try {
        // 构建 xml 文件解析器 XMLConfigBuilder
        XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
        // parse() 为具体解析的方法
        return build(parser.parse());
    } ...
}

02、XMLConfigBuilder

主要用于解析 mybatis 的 xml 配置文件,并将解析结果存放到 Configuration 对象, 得到 Configuration 对象后创建 SqlSessionFactory

XMLConfigBuilder 继承自抽象类 BaseBuilder 使用的建造者设计模式, 有兴趣可以了解下

public class XMLConfigBuilder extends BaseBuilder {
    // 是否已加载
    private boolean parsed;
    // 用来解析 xml 文件
    private final XPathParser parser;
    // 环境标识
    private String environment;
    // 核心反射工厂
    private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
}

parser.parse() 返回值为 Configuration 对象, 被定义在了 BaseBuilder

public abstract class BaseBuilder {
    protected final Configuration configuration;
    protected final TypeAliasRegistry typeAliasRegistry;
    protected final TypeHandlerRegistry typeHandlerRegistry;
}

在 XMLConfigBuilder 构造方法中进行了 configuration 的初始化工作, 初始化的步骤在下面介绍

private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
    // 执行 Configuration 无参构造
    super(new Configuration());
    ErrorContext.instance().resource("SQL Mapper Configuration");
    this.configuration.setVariables(props);
    this.parsed = false;
    this.environment = environment;
    this.parser = parser;
}

2.1 入参 XPathParser

XPathParser 是一个 xml 解析器, 具体解析器如果构建的就不描述了

2.2 入参 environment

这个参数为了保证多环境配置, 具体在 <environments> 中配置

<environments default="development">
    <environment id="development">
      xxx
    </environment>
    <environment id="test">
      xxx
    </environment>
</environments>

在 Configuration 的 parse() 方法中会解析传入的 environment 参数, 解析出 <environments> 标签下对应的 environment id

如果在 environments 标签下只有一个 environment, 而且在build()中没有指定environment; 那么一定要把 id 定义为 development, 别问我怎么知道的, NPE 真香~

这段代码是解析 mybatis-config.xml 配置文件中 environments 标签的代码

private void environmentsElement(XNode context) throws Exception {
    if (context != null) {
        // 如果传入 environment 为空, environment 为 development
        if (environment == null) {
            environment = context.getStringAttribute("default");
        }
        for (XNode child : context.getChildren()) {
            // 获取 environment 标签 id属性
            String id = child.getStringAttribute("id");
            // ✨  判断循环中 environment 是否和 XMLConfigBuilder 中定义 environment 是否一致
            if (isSpecifiedEnvironment(id)) {
                // xxx
            }
        }
    }
}

2.3 入参 Properties

Properties 就是一个继承了 Hashtable 形式的 key val 存储结构

可以在 mybatis 创建 SqlSessionFactory 时的 build(inputStream, properties) 传入

如果配置文件有 Properties 标签, build 方法也传入了 Properties 对象, 遇到相同 key 时会按照哪个呢?

实际以 XMLConfigBuilder 解析方法为准, 上源码

private void propertiesElement(XNode context) throws Exception {
    if (context != null) {
        // 读取配置文件中的 <properties>
        Properties defaults = context.getChildrenAsProperties();
        // 如果有 <properties> 标签, 查看是 resource 还是 url 引入
        String resource = context.getStringAttribute("resource");
        // 这个还真没用过
        String url = context.getStringAttribute("url");
        // 不允许同时指定
        if (resource != null && url != null) {
            throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference.  Please specify one or the other.");
        }
        // 发现 resource 或 url 方式引入, 向 defaults 添加
        if (resource != null) {
            defaults.putAll(Resources.getResourceAsProperties(resource));
        } else if (url != null) {
            defaults.putAll(Resources.getUrlAsProperties(url));
        }
        // 前面都是铺垫, 重点来了, 这里为build方法传入 Properties 参数
        Properties vars = configuration.getVariables();
        // ✨ 这里会defaults进行覆盖
        if (vars != null) {
            defaults.putAll(vars);
        }
        parser.setVariables(defaults);
        configuration.setVariables(defaults);
    }
}

到这也清楚了, build 方法传入 Properties 参数会覆盖配置文件中定义的

配置文件 Properties 参数定义, 分为两种方式: 文件引入、直接定义

直接在相关位置定义

<properties>
    <property name="driver" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://localhost:3306/test" />
    <property name="username" value="root" />
    <property name="password" value="root" />
</properties>

引入 properties 类型配置

<properties resource="jdbc.properties"/>

而代码中传递也比较简单

Properties properties = new Properties();
properties.setProperty("url", "xxx");
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream, properties);

03、Configuration 初始化

通过上面分析得知 Configuration 初始化操作在 XMLConfigBuilder

接下来看 Configuration 初始化时做了哪些动作

public Configuration() {
    // 事务
    typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
    typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
    // 数据源工厂
    typeAliasRegistry.registerAlias("JNDI", JndiDataSourceFactory.class);
    typeAliasRegistry.registerAlias("POOLED", PooledDataSourceFactory.class);
    typeAliasRegistry.registerAlias("UNPOOLED", UnpooledDataSourceFactory.class);
    // 缓存类型
    typeAliasRegistry.registerAlias("PERPETUAL", PerpetualCache.class);
    typeAliasRegistry.registerAlias("FIFO", FifoCache.class);
    typeAliasRegistry.registerAlias("LRU", LruCache.class);
    typeAliasRegistry.registerAlias("SOFT", SoftCache.class);
    typeAliasRegistry.registerAlias("WEAK", WeakCache.class);
    // 供应商的DataBaseId提供者
    typeAliasRegistry.registerAlias("DB_VENDOR", VendorDatabaseIdProvider.class);
    // 解析动态SQL的驱动, 默认XML
    typeAliasRegistry.registerAlias("XML", XMLLanguageDriver.class);
    typeAliasRegistry.registerAlias("RAW", RawLanguageDriver.class);
    // 日志类型
    typeAliasRegistry.registerAlias("SLF4J", Slf4jImpl.class);
    typeAliasRegistry.registerAlias("COMMONS_LOGGING", JakartaCommonsLoggingImpl.class);
    typeAliasRegistry.registerAlias("LOG4J", Log4jImpl.class);
    typeAliasRegistry.registerAlias("LOG4J2", Log4j2Impl.class);
    typeAliasRegistry.registerAlias("JDK_LOGGING", Jdk14LoggingImpl.class);
    typeAliasRegistry.registerAlias("STDOUT_LOGGING", StdOutImpl.class);
    typeAliasRegistry.registerAlias("NO_LOGGING", NoLoggingImpl.class);
    // 动态代理工厂
    typeAliasRegistry.registerAlias("CGLIB", CglibProxyFactory.class);
    typeAliasRegistry.registerAlias("JAVASSIST", JavassistProxyFactory.class);

    // XMLLanguageDriver 存放容器并设置默认SQL解析器
    languageRegistry.setDefaultDriverClass(XMLLanguageDriver.class);
    // RawLanguageDriver 存放容器
    languageRegistry.register(RawLanguageDriver.class);
}

总体而言 Configuration 初始化过程分为两大块

  1. 向 typeAliasRegistry 注册别名以及对应的内容
  2. 向 languageRegistry 注册 SQL 解析器并设置默认

3.1 TypeAliasRegistry

什么是类型别名 typeAlias

像 mysql、oracle 等数据库都支持别名方式, mybatis 也提供了这种机制

mybatis 中类型别名就是针对 mybatis 中常用的类型进行别名设置, 使用别名来代替具体的类型

在 mybatis 中使用 key, val 结构存放别名和实体之间的关联关系

public class TypeAliasRegistry {
    private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
}

类型别名的用途是什么

类型别名在 mybatis 中主要用于取代复杂的类型全限定名, 用于映射器配置文件中进行参数类型与返回结果类型的设置

mybatis 会在进行数据库操作之前进行参数类型别名的解析操作获取具体的参数类型, 又会在数据库操作之后进行结果类型别名的解析获取具体的结果类型

TypeAliasRegistry 初始化

在 TypeAliasRegistry 默认构造方法中将一些基本类型和其对应的引用类型进行了预置(包括相对应的数组)

public TypeAliasRegistry() {
    registerAlias("string", String.class);

    registerAlias("byte", Byte.class);
    registerAlias("long", Long.class);
    registerAlias("short", Short.class);
    registerAlias("int", Integer.class);
    registerAlias("integer", Integer.class);
    registerAlias("double", Double.class);
    registerAlias("float", Float.class);
    registerAlias("boolean", Boolean.class);

    registerAlias("byte[]", Byte[].class);
    registerAlias("long[]", Long[].class);
    registerAlias("short[]", Short[].class);
    registerAlias("int[]", Integer[].class);
    registerAlias("integer[]", Integer[].class);
    registerAlias("double[]", Double[].class);
    registerAlias("float[]", Float[].class);
    registerAlias("boolean[]", Boolean[].class);
    ...
}

自定义类型别名

除了上面所说在构造方法中预置的内容, 在项目中也可以通过 mybatis 配置文件进行配置

typeAlias 标签中的 alias 属性表示 别名, type 表示具体类型全限定名

<typeAliases>
    <typeAlias alias="jdbcLogger" type="org.apache.ibatis.logging.jdbc.BaseJdbcLogger" />
</typeAliases>

别名方式大家其实都有用过, 看一下实际中使用的; artCon 就是别名, 会配置相对应的具体类型

<select id="selectArtCons" resultType="artCon">
  ...
</select>

具体还可以通过扫描包名和配置注解的方式注册, 这里就不再赘述了

注册别名类型

在 TypeAliasRegistry 中有多个向容器存放别名的重载方法 registerAlias(), 但是真正去向容器 put 的只有一个, 这里简要分析一下

public void registerAlias(String alias, Class<?> value) {
    // alias为空抛出异常
    if (alias == null) {
        throw new TypeException("The parameter alias cannot be null");
    }
    // issue #748 这里有个修复, 有兴趣的可以去官网看一下修复内容
    // 别名统一转换为小写去比较
    String key = alias.toLowerCase(Locale.ENGLISH);
    // 如果map中存在该key 并且插入 val 与容器中已有 key 的 val不相等
    if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
        throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
    }
    // 新增别名
    TYPE_ALIASES.put(key, value);
}

3.2 LanguageDriverRegistry

LanguageDriverRegistry 是 myabtis 中的 SQL 解析容器, 将不同的 SQL 解析器进行存储, 与 TypeAliasRegistry 一致都是 HashMap 存储方式

public class TypeAliasRegistry {
    private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<String, Class<?>>();
}

Configuration 初始化时将两种不同的解析器注册到了 LanguageDriverRegistry

XMLLanguageDriver

mybatis 默认解析器, 主要作用于解析 select | update | insert | delete 节点为完整的 SQL 语句, 包括解析 <where>、<if>、<otherwise>、<when> 等动态标签, 最终创建 DynamicSqlSource

RawLanguageDriver

RawLanguageDriver 处理静态 sql, 创建出 RawSqlSource

静态 SQL 就是不包含动态标签的 SQL, 举例:

SELECT * FROM STUDENT WHERE NAME = #{NAME}

SQL 标签如何解析会单独拉篇文章来讲, 因为这一块是我比较感兴趣的

04、解析 mybatis-config.xml

4.1 parser.parse()

点进去看一下 parse() 方法的具体实现

public Configuration parse() {
    // 判断是否已解析
    if (parsed) {
        throw new BuilderException("Each XMLConfigBuilder can only be used once.");
    }
    // 设置为已解析
    parsed = true;
    // 这里为了方便打注释, 稍作了修改
    // 解析出XNode对象
    XNode xnode = parser.evalNode("/configuration");
    // 解析XNode对象, 加载核心配置
    parseConfiguration(xnode);
    return configuration;
}

4.2 parseConfiguration(xnode)

方法就是为了解析 mybatis-config.xml, 解析出各个标签并存储到 Configuration 对象

private void parseConfiguration(XNode root) {
    try {
        //issue #117 read properties first
        propertiesElement(root.evalNode("properties"));
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfs(settings);
        typeAliasesElement(root.evalNode("typeAliases"));
        pluginElement(root.evalNode("plugins"));
        objectFactoryElement(root.evalNode("objectFactory"));
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        environmentsElement(root.evalNode("environments"));
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        typeHandlerElement(root.evalNode("typeHandlers"));
        mapperElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

有没有小伙伴比较疑惑, evalNode("xxx") 都是固定的, 这些值是从哪里来的呢

这里就清楚明了了, 因为 mybatis-config.xml 仅支持这 11 个标签

至此关于 Configuration 的初始化以及关键对象赋值已说明; 里面存的属性太多了...

关于对象中的变量对象, 会单独写一篇文章

05、结语

个人感觉 写文章挺不错的, 写的过程相当于又 巩固了一遍, 而且会强迫自己说一些 通俗易懂的话, 对于之前感觉理解的内容做到 真正理解

看源码是个 枯燥的过程, 如果想学习 mybatis 框架源码, 建议先看些 相关源码视频或者书籍, 这样在你脑海中会存在一个 整体架构图

在最开始看源码时, 不能明白作者的设计; 看过视频和书籍之后, 再回首看源码的时候, 会有一种原来如此的感觉

最后总结一句话: 纸上得来终觉浅, 绝知此事要躬行

本文使用 mdnice 排版