MyBatis 源码解析:配置文件的加载与解析

上一篇我们曾约定 mybatis-config.xml 文件为配置文件,SQL 语句配置文件为映射文件,本文我们将沿用上一篇中的示例程序,一起探究一下 MyBatis 加载和解析配置文件(即 mybatis-config.xml)的过程。

配置文件的加载过程

在示例程序中,执行配置文件(包括后面要介绍的映射文件)加载与解析的过程位于第一行代码中(如下)。其中,Resources 是一个简单的基于类路径或其它位置获取数据流的工具类,借助该工具类可以获取配置文件的 InputStream 流对象,然后将其传递给 SqlSessionFactoryBuilder#build 方法以构造 SqlSessionFactory 对象。

1
2
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder()
.build(Resources.getResourceAsStream("mybatis-config.xml"));

SqlSessionFactoryBuilder 由名字可知它是一个构造器,用于构造 SqlSessionFactory 对象。按照 MyBatis 的官方文档来说,SqlSessionFactoryBuilder 一旦构造完 SqlSessionFactory 对象便完成了其使命。其实现也比较简单,只定义了 SqlSessionFactoryBuilder#build 这一个方法及其重载版本,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 创建 XML 配置文件解析器,期间会创建 Configuration 对象
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 解析配置文件填充 Configuration 对象,并基于配置构造 SqlSessionFactory
return this.build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
// 执行关闭前的清理工作
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

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

上述实现的核心在于如下两行:

1
2
1. XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
2. return this.build(parser.parse());

第一行用来构造 XMLConfigBuilder 对象,XMLConfigBuilder 可以看作是 mybatis-config.xml 配置文件的解析器;第二行则调用该对象的 XMLConfigBuilder#parse 方法对配置文件进行解析,并记录相关配置项到 Configuration 对象中,然后基于该配置对象创建 SqlSessionFactory 对象返回。Configuration 可以看作是 MyBatis 框架内部全局唯一的配置类,用于记录几乎所有的配置和映射,以及运行过程中的中间值。后面我们会经常遇到这个类,现在可以将其理解为 MyBatis 框架的配置中心。

我们来看一下 XMLConfigBuilder 对象的构造过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(
// 构造 XPath 解析器
new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()),
environment,
props);
}

private XMLConfigBuilder(XPathParser parser, // XPath 解析器
String environment, // 当前使用的配置文件组 ID
Properties props) // 参数指定的配置项
{
// 构造 Configuration 对象
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
// 将参数指定的配置项记录到 Configuration#variables 属性中
this.configuration.setVariables(props);
// 标识配置文件还未被解析
this.parsed = false;
this.environment = environment;
this.parser = parser;
}

构造方法各参数的释义见代码注释。这里针对一些比较不太直观的参数作进一步说明,首先看一下 XPathParser 类型的构造参数。我们需要知道的一点是,MyBatis 基于 DOM 树对 XML 配置文件进行解析,而操作 DOM 树的方式则是基于 XPath(XML Path Language)。它是一种能够极大简化 XML 操作的路径语言,优点在于简单、直观,并且好用,没有接触过的同学可以针对性的学习一下。XPathParser 基于 XPath 语法对 XML 进行解析,其实现比较简单,这里不展开说明。

接着看一下 environment 参数。基于配置的框架一般都允许配置多套环境,以应对开发、测试、灰度,以及生产环境。除了后面会讲到的 <environment/> 配置,MyBatis 也允许我们通过参数指定实际生效的配置环境,我们在调用 SqlSessionFactoryBuilder#build 方法时,可以以参数形式指定当前使用的配置环境。

配置文件的解析过程

完成了 XMLConfigBuilder 对象的构造,下一步会调用其 XMLConfigBuilder#parse 方法执行对配置文件的解析操作。在具体分析配置文件的解析过程之前,先简单介绍一下后续过程依赖的一些基础组件。

上面用到的 XMLConfigBuilder 类派生自 BaseBuilder 抽象类,包括后面会介绍的 XMLMapperBuilder、XMLStatementBuilder,以及 SqlSourceBuilder 等都继承自该抽象类。先来看一下 BaseBuilder 的字段定义:

1
2
3
4
5
6
/** 全局唯一的配置对象 */
protected final Configuration configuration;
/** 记录别名与类型的映射关系 */
protected final TypeAliasRegistry typeAliasRegistry;
/** 记录类型对应的类型处理器 */
protected final TypeHandlerRegistry typeHandlerRegistry;

BaseBuilder 仅定义了三个属性,各属性的作用见代码注释。XMLConfigBuilder 构造方法调用了父类 BaseBuilder 的构造方法以实现对这三个属性的初始化,前面我们提及到的封装全局配置的 Configuration 对象就记录在这里。接下来分析一下属性 BaseBuilder#typeAliasRegistryBaseBuilder#typeHandlerRegistry 分别对应的 TypeAliasRegistry 类和 TypeHandlerRegistry 类的功能和实现。

  • TypeAliasRegistry

我们都知道在编写 SQL 语句时可以为表名或列名定义别名(alias),以减少书写量,而 TypeAliasRegistry 是对别名这一机制的延伸,借助于此,我们可以为任意类型定义别名。

TypeAliasRegistry 中仅定义了一个 Map 类型的属性 TypeAliasRegistry#typeAliases 充当内存数据库,记录着别名与具体类型之间的映射关系。TypeAliasRegistry 持有一个无参数的构造方法,其中只做一件事,即调用 TypeAliasRegistry#registerAlias 方法为常用类型注册对应的别名。该方法的实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// 将别名转换成小写
String key = alias.toLowerCase(Locale.ENGLISH);
// 防止重复注册
if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'.");
}
// 建立映射关系记录到 Map 中
typeAliases.put(key, value);
}

整个方法的执行过程本质上就是将 (alias, value) 键值对写入 Map 集合中,只是在插入之前需要保证 alias 不为 null,且不允许相同的别名和类型重复注册。除了这里的单个注册,TypeAliasRegistry 还提供了 TypeAliasRegistry#registerAliases 方法,允许扫描注册指定 package 下面的所有类或指定类型及其子类型。在批量扫描注册时,我们可以利用 @Alias 注解为类指定别名,否则 MyBatis 将会以当前类的 simple name 作为类型别名。

当然,能够注册就能够获取,方法 TypeAliasRegistry#resolveAlias 提供了获取指定别名对应类型的能力。实现比较简单,无非就是从 Map 集合中获取指定 key 对应的 value。

  • TypeHandlerRegistry

再来看一下 TypeHandlerRegistry 类,在开始分析之前我们必须对 TypeHandler 接口有一个了解。我们都知道 JDBC 定义的类型(枚举类 JdbcType 对已有 JDBC 类型进行了封装)与 java 定义的类型并不是完全匹配的,所以就需要在这中间执行一些转换操作,而 TypeHandler 的职责就在于此。TypeHandler 是一个接口,其中定义了 4 个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface TypeHandler<T> {

/** 为 {@link PreparedStatement} 对象绑定参数(将数据由 java 类型转换成 JDBC 类型) */
void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;

/** 获取结果集中对应的参数值(将数据由 JDBC 类型转换成 java 类型) */
T getResult(ResultSet rs, String columnName) throws SQLException;
T getResult(ResultSet rs, int columnIndex) throws SQLException;

/** 获取存储过程中输出类型的参数值(将数据由 JDBC 类型转换成 java 类型) */
T getResult(CallableStatement cs, int columnIndex) throws SQLException;

}

围绕 TypeHandler 接口的实现类用于处理特定类型,具体可以参考 官方文档

对 TypeHandler 有一个基本认识之后,继续来看 TypeHandlerRegistry。顾名思义,这是一个 TypeHandler 的注册中心。TypeHandlerRegistry 中定义了多个 final 类型 Map 类型属性,以记录类型及其类型处理器 TypeHandler 之间的映射关系,其中最核心的两个属性定义如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 记录 JDBC 类型与 {@link TypeHandler} 之间映射关系,
* 用于从结果集读取数据时,将 JDBC 类型转换对应的 java 类型
*/
private final Map<JdbcType, TypeHandler<?>> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class);

/**
* 记录 java 类型转 JDBC 类型时所需要的 {@link TypeHandler},
* 一个 java 类型可能存在多个 JDBC 类型
*/
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();

在构造 TypeHandlerRegistry 对象时,会调用 TypeHandlerRegistry#register 方法注册类型及其对应的类型处理器,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void register(Type javaType, JdbcType jdbcType, TypeHandler<?> handler) {
// 如果 javaType 不为空,则添加对应的类型处理器到 typeHandlerMap 集合中
if (javaType != null) {
Map<JdbcType, TypeHandler<?>> map = typeHandlerMap.get(javaType);
if (map == null || map == NULL_TYPE_HANDLER_MAP) {
map = new HashMap<>();
}
map.put(jdbcType, handler);
typeHandlerMap.put(javaType, map);
}
// 记录所有的 TypeHandler 对象
allTypeHandlersMap.put(handler.getClass(), handler);
}

上述方法的核心逻辑在于往 TypeHandlerRegistry#typeHandlerMap 属性中注册 java 类型及其类型处理器。MyBatis 基于该方法封装了多层重载版本,其中大部分实现都比较简单,下面就基于注解 @MappedJdbcTypes 和注解 @MappedTypes 指定对应类型的版本进一步说明。

注解 @MappedJdbcTypes 用于指定类型处理器 TypeHandler 关联的 JDBC 类型列表,对应的解析实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private <T> void register(Type javaType, TypeHandler<? extends T> typeHandler) {
// 获取 MappedJdbcTypes 注解配置
MappedJdbcTypes mappedJdbcTypes = typeHandler.getClass().getAnnotation(MappedJdbcTypes.class);
if (mappedJdbcTypes != null) {
// 一个 TypeHandler 可以关联多个 JDBC 类型,遍历逐一注册
for (JdbcType handledJdbcType : mappedJdbcTypes.value()) {
this.register(javaType, handledJdbcType, typeHandler);
}
// 允许处理 null 值
if (mappedJdbcTypes.includeNullJdbcType()) {
this.register(javaType, null, typeHandler);
}
} else {
this.register(javaType, null, typeHandler);
}
}

上述方法首先获取注解 @MappedJdbcTypes 配置的 JDBC 类型列表,然后遍历挨个注册。注解 @MappedJdbcTypes 定义如下:

1
2
3
4
5
6
7
8
9
10
11
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MappedJdbcTypes {

/** 当前类型处理器能够处理的 JDBC 类型列表 */
JdbcType[] value();

/** 是否允许处理 null 值 */
boolean includeNullJdbcType() default false;
}

该注解还允许通过 MappedJdbcTypes#includeNullJdbcType 属性指定是否允许当前类型处理器处理 null 值。

能够指定 JDBC 类型,当然也就能够指定 JAVA 类型。注解 @MappedTypes 用于指定与类型处理器 TypeHandler 关联的 java 类型,对应的解析实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public <T> void register(TypeHandler<T> typeHandler) {
boolean mappedTypeFound = false;
// 获取 MappedTypes 注解配置
MappedTypes mappedTypes = typeHandler.getClass().getAnnotation(MappedTypes.class);
if (mappedTypes != null) {
// 一个 TypeHandler 可以关联多个 java 类型,遍历逐一注册
for (Class<?> handledType : mappedTypes.value()) {
this.register(handledType, typeHandler);
mappedTypeFound = true;
}
}
// 尝试基于 typeHandler 自动发现对应的 java 类型,需要实现 TypeReference 接口(@since 3.1.0)
if (!mappedTypeFound && typeHandler instanceof TypeReference) {
try {
TypeReference<T> typeReference = (TypeReference<T>) typeHandler;
this.register(typeReference.getRawType(), typeHandler);
mappedTypeFound = true;
} catch (Throwable t) {
// maybe users define the TypeReference with a different type and are not assignable, so just ignore it
}
}
if (!mappedTypeFound) {
this.register((Class<T>) null, typeHandler);
}
}

上述方法首先获取 @MappedTypes 注解配置,并针对关联的 java 类型逐一注册。如果未指定 @MappedTypes 注解配置,则 MyBatis 会尝试自动发现并注册 TypeHandler 能够处理的 java 类型。

能够注册也就能够获取,TypeHandlerRegistry 中提供了 TypeHandlerRegistry#getTypeHandler 方法的多种重载实现,比较简单,不再展开。

回过头再来看一下 BaseBuilder 抽象类的实现,其中定义了许多方法,但是只要了解上面介绍的 TypeAliasRegistry 和 TypeHandlerRegistry 类,那么这些方法的作用在理解上应该非常容易,这里就不多做撰述,有兴趣的同学可以参考上面的分析去阅读一下源码。

下面正式进入主题,回到 XMLConfigBuilder#parse 方法分析配置文件的解析过程,实现如下:

1
2
3
4
5
6
7
8
9
10
public Configuration parse() {
// 配置文件已经被解析过,避免重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 解析 mybatis-config.xml 中的各项配置,填充 Configuration 对象
this.parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

配置文件 mybatis-config.xml<configuration/> 标签作为配置文件根节点,上述方法的核心在于触发调用 XMLConfigBuilder#parseConfiguration 方法对配置文件的各个元素进行解析,并封装解析结果到 Configuration 对象中,最终返回该配置对象。方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void parseConfiguration(XNode root) {
try {
// 解析 <properties/> 配置
this.propertiesElement(root.evalNode("properties"));
// 解析 <settings/> 配置
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
// 获取并设置 vfsImpl 属性
this.loadCustomVfs(settings);
// 获取并设置 logImpl 属性
this.loadCustomLogImpl(settings);
// 解析 <typeAliases/> 配置
this.typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins/> 配置
this.pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory/> 配置
this.objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory/> 配置
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory/> 配置
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 将 settings 配置设置到 Configuration 对象中
this.settingsElement(settings);
// 解析 <environments/> 配置
this.environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider/> 配置
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers/> 配置
this.typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers/> 配置
this.mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

上述方法在实现上比较直观,各配置项的解析都采用专门的方法进行封装,接下来会逐一进行分析。其中 <plugins/> 标签用于配置自定义插件,以拦截 SQL 语句的执行过程,相应的解析过程暂时先不展开,留到后面专门介绍插件的实现机制的文章中一并分析。

解析 properties 标签

先来看一下 <properties/> 标签怎么玩,其中的配置项可以在整个配置文件中用来动态替换占位符。配置项可以从外部 properties 文件读取,也可以通过 <property/> 子标签指定。假设我们希望通过该标签指定数据源配置,如下:

1
2
3
4
5
<properties resource="datasource.properties">
<property name="driver" value="com.mysql.jdbc.Driver"/>
<!--为占位符启用默认值配置,默认关闭,需要采用如下方式开启-->
<property name="org.apache.ibatis.parsing.PropertyParser.enable-default-value" value="true"/>
</properties>

文件 datasource.properties 内容:

1
2
3
url=jdbc:mysql://localhost:3306/test
username=root
password=123456

然后可以基于 OGNL 表达式在其它配置项中引用这些配置值,如下:

1
2
3
4
5
6
<dataSource type="POOLED"> <!--or UNPOOLED or JNDI-->
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username:zhenchao}"/> <!--占位符设置默认值,需要专门开启-->
<property name="password" value="${password}"/>
</dataSource>

其中,除了 driver 属性值来自 <property/> 子标签,其余属性值均是从 datasource.properties 配置文件中获取的。

MyBatis 针对配置的读取顺序约定如下:

  1. <properties/> 标签体内指定的属性首先被读取;
  2. 然后,根据 <properties/> 标签中 resource 属性读取类路径下配置文件,或根据 url 属性指定的路径读取指向的配置文件,并覆盖已读取的同名配置项;
  3. 最后,读取方法参数传递的配置项,并覆盖已读取的同名配置项。

下面分析一下 <properties/> 标签的解析过程,由 XMLConfigBuilder#propertiesElement 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 获取 <property/> 子标签列表,封装成 Properties 对象
Properties defaults = context.getChildrenAsProperties();
// 支持通过 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.");
}
// 从类路径加载配置文件
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
}
// 从 url 指定位置加载配置文件
else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 合并已有的配置项
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 填充 XPathParser 和 Configuration 对象
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}

由 MyBatis 的官方文档可知,标签 <properties/> 支持以 resource 属性或 url 属性指定配置文件所在的路径,由上述实现也可以看出这两个属性配置是互斥的。在将对应的配置加载成为 Properties 对象之后,上述方法会合并 Configuration 对象中已有的配置项,并将结果再次填充到 XPathParser 和 Configuration 对象中,以备后用。

解析 settings 标签

MyBatis 通过 <settings/> 标签提供一些全局性的配置,这些配置会影响 MyBatis 的运行行为。官方文档 对这些配置项进行了详细的说明,下面的配置摘自官方文档,其中各项的含义可以参考文档说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="multipleResultSetsEnabled" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="useGeneratedKeys" value="false"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
<setting name="autoMappingUnknownColumnBehavior" value="WARNING"/>
<setting name="defaultExecutorType" value="SIMPLE"/>
<setting name="defaultStatementTimeout" value="25"/>
<setting name="defaultFetchSize" value="100"/>
<setting name="safeRowBoundsEnabled" value="false"/>
<setting name="mapUnderscoreToCamelCase" value="false"/>
<setting name="localCacheScope" value="SESSION"/>
<setting name="jdbcTypeForNull" value="OTHER"/>
<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString"/>
</settings>

MyBatis 对于该标签的解析实现十分简单,首先调用 XMLConfigBuilder#settingsAsProperties 方法获取配置项对应的 Properties 对象,同时会检查配置项是否是可识别的,实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
}
// 解析 <setting/> 配置,封装成 Properties 对象
Properties props = context.getChildrenAsProperties();
// 构造 Configuration 对应的 MetaClass 对象,用于对 Configuration 类提供反射操作
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
// 遍历配置项,确保配置项是 MyBatis 可识别的
for (Object key : props.keySet()) {
// 属性对应的 setter 方法不存在
if (!metaConfig.hasSetter(String.valueOf(key))) {
throw new BuilderException(
"The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}

接下来调用 XMLConfigBuilder#loadCustomVfs 方法和 XMLConfigBuilder#loadCustomLogImpl 方法分别解析 vfsImpllogImpl 配置项,其中 vfsImpl 配置项用于设置自定义 VFS 的实现类全限定名,以逗号分隔。所有的 <settings/> 配置项最后都会通过 XMLConfigBuilder#settingsElement 方法记录到 Configuration 对象对应的属性中。

解析 typeAliases 和 typeHandlers 标签

前面介绍了 TypeAliasRegistry 和 TypeHandlerRegistry 两个类的功能和实现,本小节介绍的这两个标签分别对应这两个类的相关配置,前者用于配置类型及其别名的映射关系,后者用于配置类型及其类型处理器 TypeHandler 之间的映射关系。二者在实现上基本相同,这里以 <typeAliases/> 标签的解析过程为例进行分析(由 XMLConfigBuilder#typeAliasesElement 方法实现),有兴趣的读者可以自己阅读 <typeHandlers/> 标签的相关实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 子标签是 <package name=""/> 配置
if ("package".equals(child.getName())) {
/*
* 如果指定了一个包名,MyBatis 会在包名下搜索需要的 Java Bean,并处理 @Alias 注解,
* 在没有注解的情况下,会使用 Bean 的首字母小写的简单名称作为它的别名。
*/
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
}
// 子标签是 <typeAlias alias="" type=""/> 配置
else {
String alias = child.getStringAttribute("alias"); // 别名
String type = child.getStringAttribute("type"); // 类型限定名
try {
// 获取类型对应的 Class 对象
Class<?> clazz = Resources.classForName(type);
// 未配置 alias,先尝试获取 @Alias 注解,如果没有则使用类的简单名称
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
}
// 配置了 alias,使用该 alias 进行注册
else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}

标签 <typeAliases/> 具备两种配置方式,单一注册与批量扫描,具体使用可以参考 官方文档。对应的实现也需要区分这两种情况,如果是批量扫描,即子标签是 <package/>,则会调用 TypeAliasRegistry#registerAliases 方法进行扫描注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void registerAliases(String packageName) {
this.registerAliases(packageName, Object.class);
}

public void registerAliases(String packageName, Class<?> superType) {
// 获取指定 package 下所有 superType 类型及其子类型
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> typeSet = resolverUtil.getClasses();
// 遍历处理扫描到的类型
for (Class<?> type : typeSet) {
// 忽略内部类、接口,以及抽象类
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// 尝试获取类的 @Alias 注解,如果没有则使用类的简单名称的小写形式作为别名进行注册
this.registerAlias(type);
}
}
}

如果子标签是 <typeAlias alias="" type=""/> 这种配置形式,则会获取 alias 和 type 属性值,然后基于一定规则进行注册,具体过程如代码注释。

解析 objectFactory 标签

在具体分析 <objectFactory/> 标签的解析实现之前,我们必须先了解与之密切相关的 ObjectFactory 接口。由名字我们可以猜测这是一个工厂类,并且是创建对象的工厂,定义如下:

1
2
3
4
5
6
7
8
9
10
public interface ObjectFactory {
/** 设置配置信息 */
default void setProperties(Properties properties) { }
/** 基于无参构造方法创建指定类型对象 */
<T> T create(Class<T> type);
/** 基于指定的构造参数(类型)选择对应的构造方法创建目标对象 */
<T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs);
/** 检测指定类型是否是集合类型 */
<T> boolean isCollection(Class<T> type);
}

各方法的作用如代码注释,DefaultObjectFactory 类是该接口的默认实现。下面重点看一下基于指定构造参数(类型)选择对应的构造方法创建目标对象的实现细节:

1
2
3
4
5
6
public <T> T create(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
// 如果传入的是接口类型,则选择具体的实现类型以创建对象,毕竟接口类型不能被实例化
Class<?> classToCreate = this.resolveInterface(type);
// 基于入参选择合适的构造方法进行实例化
return (T) this.instantiateClass(classToCreate, constructorArgTypes, constructorArgs);
}

方法首先会判断当前指定的类型是否是接口类型,因为接口类型无法实例化,所以需要选择相应的实现类代替。例如当我们传递的是一个 List 接口类型会返回相应的 ArrayList 实现类型。再来看一下 DefaultObjectFactory#instantiateClass 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
private <T> T instantiateClass(Class<T> type, List<Class<?>> constructorArgTypes, List<Object> constructorArgs) {
try {
Constructor<T> constructor;
// 如果没有传递构造参数或类型,则使用无参构造方法创建对象
if (constructorArgTypes == null || constructorArgs == null) {
constructor = type.getDeclaredConstructor();
try {
return constructor.newInstance();
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance();
} else {
throw e;
}
}
}
// 否则选择对应的构造方法创建对象
constructor = type.getDeclaredConstructor(constructorArgTypes.toArray(new Class[0]));
try {
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} catch (IllegalAccessException e) {
if (Reflector.canControlMemberAccessible()) {
constructor.setAccessible(true);
return constructor.newInstance(constructorArgs.toArray(new Object[0]));
} else {
throw e;
}
}
} catch (Exception e) {
// ... 异常处理略
}
}

上述方法主要基于传递的参数以决策具体创建对象的构造方法版本,并基于反射机制创建对象。

所以说 ObjectFactory 接口的作用主要是对我们传递的类型进行实例化,默认的实现版本比较简单。如果默认实现不能满足需求,则可以扩展 ObjectFactory 接口,并将相应的自定义实现通过 <objectFactory/> 标签进行注册,具体的使用方式参见 官方文档。我们继续分析针对该标签的解析过程,由 XMLConfigBuilder#objectFactoryElement 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void objectFactoryElement(XNode context) throws Exception {
if (context != null) {
// 获取 type 属性配置,对应自定义对象工厂类
String type = context.getStringAttribute("type");
// 获取 <property/> 子标签列表,封装成 Properties 对象
Properties properties = context.getChildrenAsProperties();
// 实例化自定义工厂类对象
ObjectFactory factory = (ObjectFactory) this.resolveClass(type).getDeclaredConstructor().newInstance();
// 设置属性配置
factory.setProperties(properties);
// 填充 Configuration 对象
configuration.setObjectFactory(factory);
}
}

解析 <objectFactory/> 标签的基本流程就是获取我们在标签中通过 type 属性指定的自定义 ObjectFactory 实现类的全限定名和相应属性配置;然后构造自定义 ObjectFactory 实现类对象,并将获取到的配置项列表记录到对象中;最后将自定义 ObjectFactory 对象填充到 Configuration 对象中。

解析 reflectorFactory 标签

标签 <reflectorFactory/> 用于注册自定义 ReflectorFactory 实现,该标签的解析过程与 <objectFactory/> 标签基本相同,不再重复撰述,本小节重点分析一下该标签涉及到相关类的功能与实现。

ReflectorFactory 顾名思义是一个 Reflector 工厂,接口定义如下:

1
2
3
4
5
6
7
8
public interface ReflectorFactory {
/** 是否缓存 {@link Reflector} 对象 */
boolean isClassCacheEnabled();
/** 设置是否缓存 {@link Reflector} 对象 */
void setClassCacheEnabled(boolean classCacheEnabled);
/** 获取指定类型的 {@link Reflector} 对象 */
Reflector findForClass(Class<?> type);
}

默认实现类 DefaultReflectorFactory 通过一个 boolean 变量 DefaultReflectorFactory#classCacheEnabled 记录是否启用缓存,并通过一个线程安全的 Map 集合 DefaultReflectorFactory#reflectorMap 记录缓存的 Reflector 对象,相应的方法实现都十分简单,不再展开。

ReflectorFactory 本质上是用来创建和管理 Reflector 对象,那么 Reflector 又是什么呢?我们先来看一下 Reflector 的属性和构造方法定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Reflector {

/** 隶属的 Class 类型 */
private final Class<?> type;
/** 可读属性名称集合 */
private final String[] readablePropertyNames;
/** 可写属性名称集合 */
private final String[] writablePropertyNames;
/** 属性对应的 setter 方法(封装成 Invoker 对象) */
private final Map<String, Invoker> setMethods = new HashMap<String, Invoker>();
/** 属性对应的 getter 方法(封装成 Invoker 对象) */
private final Map<String, Invoker> getMethods = new HashMap<String, Invoker>();
/** 属性对应 setter 方法的入参类型 */
private final Map<String, Class<?>> setTypes = new HashMap<String, Class<?>>();
/** 属性对应 getter 方法的返回类型 */
private final Map<String, Class<?>> getTypes = new HashMap<String, Class<?>>();
/** 记录默认构造方法 */
private Constructor<?> defaultConstructor;
/** 记录所有的属性名称 */
private Map<String, String> caseInsensitivePropertyMap = new HashMap<String, String>();

public Reflector(Class<?> clazz) {
type = clazz;
// 解析获取默认构造方法(无参构造方法)
this.addDefaultConstructor(clazz);
// 解析获取所有的 getter 方法,并记录到 getMethods 与 getTypes 属性中
this.addGetMethods(clazz);
// 解析获取所有的 setter 方法,并记录到 setMethods 与 setTypes 属性中
this.addSetMethods(clazz);
// 解析获取所有没有 setter/getter 方法的字段,并添加到相应的集合中
this.addFields(clazz);
// 填充可读属性名称数组
readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]);
// 填充可写属性名称数组
writablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]);
// 记录所有属性名称到 Map 集合中
for (String propName : readablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
for (String propName : writablePropertyNames) {
caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName);
}
}
// ... 省略方法实现
}

可以看到 Reflector 是对指定 Class 对象的封装,记录了对应的 Class 类型、属性、getter 和 setter 方法列表等信息,是反射操作的基础,其中的方法实现虽然较长,但是逻辑都比较简单,读者可以自行阅读源码。

解析 objectWrapperFactory 标签

标签 <objectWrapperFactory/> 用于注册自定义 ObjectWrapperFactory 实现,该标签的解析过程与 <objectFactory/> 标签基本相同,同样不再重复撰述,本小节重点分析该标签涉及到相关类的功能与实现。

ObjectWrapperFactory 顾名思义是一个 ObjectWrapper 工厂,其默认实现 DefaultObjectWrapperFactory 并没有实现有用的逻辑,所以可以忽略。然而,借助 <reflectorFactory/> 标签,我们可以注册自定义的 ObjectWrapperFactory 实现。

被 ObjectWrapperFactory 创建和管理的 ObjectWrapper 是一个接口,用于包装和处理对象,其中声明了多个操作对象的方法,包括获取、更新对象属性等,接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface ObjectWrapper {
/** 获取对应属性的值(对于集合而言,则是获取对应下标的值) */
Object get(PropertyTokenizer prop);
/** 设置对应属性的值(对于集合而言,则是设置对应下标的值)*/
void set(PropertyTokenizer prop, Object value);
/** 查找属性表达式对应的属性 */
String findProperty(String name, boolean useCamelCaseMapping);
/** 获取可读属性名称集合 */
String[] getGetterNames();
/** 获取可写属性名称集合 */
String[] getSetterNames();
/** 获取属性表达式指定属性 setter 方法的入参类型 */
Class<?> getSetterType(String name);
/** 获取属性表达式指定属性 getter 方法的返回类型 */
Class<?> getGetterType(String name);
/** 判断属性是否有 setter 方法 */
boolean hasSetter(String name);
/** 判断属性是否有 getter 方法 */
boolean hasGetter(String name);
/** 为属性表达式指定的属性创建对应的 {@link MetaObject} 对象 */
MetaObject instantiatePropertyValue(String name, PropertyTokenizer prop, ObjectFactory objectFactory);
/** 是否是 {@link java.util.Collection} 类型 */
boolean isCollection();
/** 调用 {@link java.util.Collection} 对应的 add 方法 */
void add(Object element);
/** 调用 {@link java.util.Collection} 对应的 addAll 方法 */
<E> void addAll(List<E> element);
}

由接口定义可以看出,ObjectWrapper 的主要作用在于简化调用方对于对象的操作。

解析 environments 标签

标签 <environments/> 用于配置多套数据库环境,典型的应用场景就是在开发、测试、灰度,以及生产等环境通过该标签分别指定相应的配置。当应用需要同时操作多套数据源时,也可以基于该标签分别配置,具体的使用请参阅 官方文档。MyBatis 解析该标签的过程由 XMLConfigBuilder#environmentsElement 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
// 未通过参数指定生效的 environment 配置,获取 default 属性值
if (environment == null) {
environment = context.getStringAttribute("default");
}
// 遍历处理 <environment/> 子标签
for (XNode child : context.getChildren()) {
// 获取 id 属性配置
String id = child.getStringAttribute("id");
// 处理指定生效的 <environment/> 配置
if (this.isSpecifiedEnvironment(id)) {
// 处理 <transactionManager/> 子标签
TransactionFactory txFactory = this.transactionManagerElement(child.evalNode("transactionManager"));
// 处理 <dataSource/> 子标签
DataSourceFactory dsFactory = this.dataSourceElement(child.evalNode("dataSource"));
// 基于解析到的值构造 Environment 对象填充 Configuration 对象
DataSource dataSource = dsFactory.getDataSource();
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}

上述方法首先会判断是否通过参数指定了 environment 配置,如果没有则尝试获取 <environments/> 标签的 default 属性,说明参数指定相对于 default 属性配置优先级更高。然后开始遍历寻找并解析指定激活的 <environment/> 配置。整个解析过程主要是对 <transactionManager/><dataSource/> 两个子标签进行解析,前者用于指定 MyBatis 的事务管理器,后者用于配置数据源。

数据源的配置解析比较直观,下面主要看一下事务管理器配置的解析过程,由 XMLConfigBuilder#transactionManagerElement 方法实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
private TransactionFactory transactionManagerElement(XNode context) throws Exception {
if (context != null) {
// 获取事务管理器类型配置:JDBC or MANAGED
String type = context.getStringAttribute("type");
// 获取 <property/> 子标签列表,封装成 Properties 对象
Properties props = context.getChildrenAsProperties();
// 构造对应的 TransactionFactory 对象,并填充属性值
TransactionFactory factory = (TransactionFactory) this.resolveClass(type).getDeclaredConstructor().newInstance();
factory.setProperties(props);
return factory;
}
throw new BuilderException("Environment declaration requires a TransactionFactory.");
}

MyBatis 允许我们配置两种类型的事务管理器,即 JDBC 类型和 MANAGED 类型,引用官方文档的话来理解二者的区别:

在 MyBatis 中有两种类型的事务管理器(也就是 type="[JDBC|MANAGED]"):

  • JDBC:这个配置直接使用了 JDBC 的提交和回滚设施,它依赖从数据源获得的连接来管理事务作用域。
  • MANAGED:这个配置几乎没做什么。它从不提交或回滚一个连接,而是让容器来管理事务的整个生命周期(比如 JEE 应用服务器的上下文)。 默认情况下它会关闭连接。然而一些容器并不希望连接被关闭,因此需要将 closeConnection 属性设置为 false 来阻止默认的关闭行为。例如:
1
2
3
<transactionManager type="MANAGED">
<property name="closeConnection" value="false"/>
</transactionManager>

提示:如果你正在使用 Spring + MyBatis,则没有必要配置事务管理器,因为 Spring 模块会使用自带的管理器来覆盖前面的配置。

Transaction 接口定义了事务,并为 JDBC 类型和 MANAGED 类型提供了相应的实现,即 JdbcTransaction 和 ManagedTransaction。正如上面引用的官方文档所说的那样,MyBatis 的事务操作实现的比较简单,考虑实际应用中更多是依赖于 Spring 的事务管理器,这里也就不再深究。

解析 databaseIdProvider 标签

生产环境中可能会存在同时操作多套不同类型数据库的场景,而 <databaseIdProvider/> 标签则用于配置数据库厂商标识。我们知道 SQL 不能完全做到数据库无关,且 MyBatis 暂时也还不能做到对上层完全屏蔽底层数据库的实现细节,所以在这种情况下执行 SQL 语句时,我们需要通过 databaseId 指定 SQL 应用的具体数据库类型。

该标签的解析过程由 XMLConfigBuilder#databaseIdProviderElement 方法实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void databaseIdProviderElement(XNode context) throws Exception {
DatabaseIdProvider databaseIdProvider = null;
if (context != null) {
String type = context.getStringAttribute("type");
// awful patch to keep backward compatibility
if ("VENDOR".equals(type)) {
type = "DB_VENDOR"; // 保持兼容
}
// 获取 <property/> 子节点配置
Properties properties = context.getChildrenAsProperties();
// 构造 DatabaseIdProvider 对象
databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance();
// 设置配置的属性
databaseIdProvider.setProperties(properties);
}
Environment environment = configuration.getEnvironment();
if (environment != null && databaseIdProvider != null) {
// 获取当前数据库环境对应的 databaseId,并记录到 Configuration.databaseId 中,已备后用
String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource());
configuration.setDatabaseId(databaseId);
}
}

解析过程如上述代码注释,关于该标签的使用可以参考 官方文档

解析 mappers 标签

标签 <mappers/> 用于指定映射文件列表,这是一个我们非常熟悉的标签。MyBatis 广受欢迎的一个很重要的原因是支持自己定义 SQL 语句,这样就可以保证 SQL 的优化可控。抛去注解配置 SQL 的形式(注解对于复杂 SQL 的支持较弱,一般仅用于编写简单的 SQL),对于框架自动生成的 SQL 和用户自定义的 SQL 都记录在映射 XML 文件中,标签 <mappers/> 用于指定映射文件所在的路径。

我们可以通过 <mapper resource=""><mapper url=""> 子标签指定映射 XML 文件所在的位置,也可以通过 <mapper class=""> 子标签指定一个或多个具体的 Mapper 接口,甚至可以通过 <package name=""/> 子标签指定映射文件所在的包名,扫描注册。

该标签的解析过程由 XMLConfigBuilder#mapperElement 方法实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
/*
* 配置了 package 属性,从指定包下面扫描注册
* <mappers>
* <package name="org.mybatis.builder"/>
* </mappers>
*/
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
// 调用 MapperRegistry 进行注册
configuration.addMappers(mapperPackage);
}
// 处理 resource、url,以及 class 配置的场景
else {
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
/*
* <!-- Using classpath relative resources -->
* <mappers>
* <mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
* <mapper resource="org/mybatis/builder/BlogMapper.xml"/>
* <mapper resource="org/mybatis/builder/PostMapper.xml"/>
* </mappers>
*/
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
// 从类路径获取文件输入流
InputStream inputStream = Resources.getResourceAsStream(resource);
// 构建 XMLMapperBuilder 对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 执行映射文件解析
mapperParser.parse();
}
/*
* <!-- Using url fully qualified paths -->
* <mappers>
* <mapper url="file:///var/mappers/AuthorMapper.xml"/>
* <mapper url="file:///var/mappers/BlogMapper.xml"/>
* <mapper url="file:///var/mappers/PostMapper.xml"/>
* </mappers>
*/
else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 基于 url 获取配置文件输入流
InputStream inputStream = Resources.getUrlAsStream(url);
// 构建 XMLMapperBuilder 对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 执行映射文件解析
mapperParser.parse();
}
/*
* <!-- Using mapper interface classes -->
* <mappers>
* <mapper class="org.mybatis.builder.AuthorMapper"/>
* <mapper class="org.mybatis.builder.BlogMapper"/>
* <mapper class="org.mybatis.builder.PostMapper"/>
* </mappers>
*/
else if (resource == null && url == null && mapperClass != null) {
// 获取指定接口 Class 对象
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 调用 MapperRegistry 进行注册
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

上述方法首先会判断当前是否是 package 配置,如果是则会获取配置的 package 名称,然后执行扫描注册逻辑。如果是 resource 或 url 配置,则先获取指定路径映射文件的输入流,然后构造 XMLMapperBuilder 对象对映射文件进行解析。对于 class 配置而言,则会构建接口限定名对应的 Class 对象,并调用 MapperRegistry#addMapper 方法执行注册。

整个方法的运行逻辑还是比较直观的,其中涉及到对映射文件的解析注册过程,即 XMLMapperBuilder 相关类实现,将留到下一篇介绍映射文件加载与解析时专门介绍。

下面来重点分析一下 MapperRegistry 类及其周边类的功能和实现。我们在使用 MyBatis 框架时需要实现数据表对应的 Mapper 接口(以后统称为 Mapper 接口),其中声明了一系列数据库操作方法。我们可以通过注解的方式在方法上编写 SQL 语句,也可以通过映射 XML 文件的方式编写和关联对应的 SQL 语句。上面解析 <mappers/> 标签实现时我们看到方法通过调用 MapperRegistry#addMapper 方法注册相应的 Mapper 接口,包括以 package 配置的方式在扫描获取到相应的 Mapper 接口之后,也需要通过调用该方法进行注册。MapperRegistry 类中定义了两个属性:

1
2
3
4
/** 全局唯一配置对象 */
private final Configuration config;
/** 记录 Mapper 接口(Class 对象)与 {@link MapperProxyFactory} 之间的映射关系 */
private final Map<Class<?>, MapperProxyFactory<?>> knownMappers = new HashMap<>();

上面调用的 MapperRegistry#addMapper 方法实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {
// 对应 Mapper 接口已注册
if (this.hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
// 标记整个过程是否成功完成
boolean loadCompleted = false;
try {
// 注册 Mapper 接口 Class 对象与 MapperProxyFactory 之间的映射关系
knownMappers.put(type, new MapperProxyFactory<>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析 Mapper 接口中的注解 SQL 配置
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

Mapper 方法必须是一个接口才会被注册,这主要是为了配合 JDK 内置的动态代理机制。上一篇介绍 MyBatis 的基本运行原理时我们曾说过,MyBatis 通过为 Mapper 接口创建相应的动态代理类以执行具体的数据库操作,这一部分的详细过程将留到后面介绍 SQL 语句执行机制时再细讲,这里先知道有这样一个概念即可。如果当前 Mapper 接口还没有被注册,则会创建对应的 MapperProxyFactory 对象并记录到 MapperRegistry#knownMappers 属性中,然后解析 Mapper 接口中注解的 SQL 配置,这一过程留到下一篇分析映射文件解析过程时再一并介绍。

总结

到此,我们完成了对配置文件 mybatis-config.xml 加载和解析过程的分析。总的来说,对于配置文件的解析实际上就是将静态的 XML 配置解析成内存中的 Configuration 对象的过程。Configuration 可以看作是 MyBatis 全局的配置中心,后续对于映射文件的解析,以及 SQL 语句的执行都依赖于其中的配置项。下一篇,我们将一起来探究映射文件的加载和解析过程。

参考

  1. MyBatis 官方文档
  2. MyBatis 技术内幕