深入浅出MyBatis 源码:配置文件解析

本文将会介绍MyBatis配置文件解析部分的代码解读,从创建一个SqlSessionFactory作为入口,引入MyBatis配置文件的说明。简要说明配置文件中常用标签的用法和说明,根据每个标签,详细介绍MyBatis是如何解析这些标签的。

创建一个SqlSessionFactory的几种方式

每个基于 MyBatis 的应用都是以一个 SqlSessionFactory 的实例为核心的。SqlSessionFactory 的实例可以通过 SqlSessionFactoryBuilder 获得。而 SqlSessionFactoryBuilder 则可以从 XML 配置文件或一个预先定制的 Configuration 的实例构建出 SqlSessionFactory 的实例。

1
2
3
String resource = "org/mybatis/example/mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

从上文的实例代码可以看出,sqlSessionFactory 实例是通过SqlSessionFactoryBuilderbuild 方法构建出来的。我们进入SqlSessionFactoryBuilder 中可以看到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class SqlSessionFactoryBuilder {

public SqlSessionFactory build(Reader reader){...};
public SqlSessionFactory build(Reader reader, String environment){...};
public SqlSessionFactory build(Reader reader, Properties properties){...};
public SqlSessionFactory build(Reader reader, String environment, Properties properties){...};

public SqlSessionFactory build(InputStream inputStream){...};
public SqlSessionFactory build(InputStream inputStream, String environment){...};
public SqlSessionFactory build(InputStream inputStream, Properties properties){...};
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties){...};

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

SqlSessionFactoryBuilder拥有9build 重载方法,大概可以分为两类:

  • 以XML配置文件输入流的方式,如Read 字符流或 InputStream字节流
  • 预先实例化一个Configuration 实例

SqlSessionFactoryBuilder 从命名可以看出就是SqlSessionFactory构建器,功能是去构建出SqlSessionFactory实例,而SqlSessionFactory 再去构建出SqlSession , 这个SqlSession 可以理解为数据库客户端连接服务端的会话。在现实生活中,我们是知道数据库服务器的主机地址,端口,用户名,密码等信息的,对应用而言,也应该有个「地方」去记录这些配置信息,在MyBatis中,这个「地方」就是「配置文件」,一般命名为mybatis-config.xml。XML文件是文件层面的「配置」,而Configuration 类是Java Class层面上的「配置」。

MyBatis 配置文件的使用

一般MyBatis 的配置文件是一个XML文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>
<mappers>
<mapper resource="org/mybatis/example/BlogMapper.xml"/>
</mappers>
</configuration>

大概可以分为两部分:

  • XML头部声明DTD文件(Document Type Definition):用于验证格式的正确性,关于DTD的介绍可以查看这里
  • configuration为头节点的配置节点树:用于设置MyBatis的行为和属性

这里稍微多说一句,配置文件的头部声明是HTTP协议的,那是不是意味着校验XML合法性时必须请求网络一次?
在初始化XMLConfigBuilder时,发现也同时传入了一个XMLMapperEntityResolver实例。

1
2
3
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(inputStream, true, props, new XMLMapperEntityResolver()), environment, props);
}

可以看到XMLMapperEntityResolver#resolveEntity方法中,会直接读取存在本地的dtd文件,这样保证了就算处于离线环境依旧可以成功校验配置文件。

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
/**
* 本地 mybatis-config.dtd 文件
*/
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
/**
* 本地 mybatis-mapper.dtd 文件
*/
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";

@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
// 本地 mybatis-config.dtd 文件
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
// 本地 mybatis-mapper.dtd 文件
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}

文档得知全部配置如下,关于配置的解释和使用这里不作具体说明,文档的解释比较详细。

  • configuration(配置)
    • properties(属性)
    • settings(设置)
    • typeAliases(类型别名)
    • typeHandlers(类型处理器)
    • objectFactory(对象工厂)
    • plugins(插件)
    • environments(环境配置)
    • environment(环境变量)
    • transactionManager(事务管理器)
    • dataSource(数据源)
    • databaseIdProvider(数据库厂商标识)
    • mappers(映射器)

MyBatis 配置文件的使用的parse 过程

MyBatis 配置文件的解析就是将XML配置转换为Configuration实例的过程。可以分为两步:

  • 第一步:通过XPathParser将XML转换为org.w3c.dom.Document对象
  • 第二步:通过eval 节点属性,设置Configuration属性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 创建 XMLConfigBuilder 对象,执行 XML 解析
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);

// 创建 DefaultSqlSessionFactory 对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

回过头再看SqlSessionFactoryBuilder#build方法,创建 XMLConfigBuilder 对象时就完成转换为Document对象的过程,parser.parse()做的就是构造Configuration实例。

创建 org.w3c.dom.Document 对象

XMLConfigBuilder持有XPathParserXPathParser持有Document,在构造XMLConfigBuilder时,同时也会构造XPathParser,在构造XPathParser时通过调用createDocument方法设置了该属性。

1
2
3
4
public XPathParser(InputStream inputStream, boolean validation, Properties variables) {
commonConstructor(validation, variables, null);
this.document = createDocument(new InputSource(inputStream));
}

构造Configuration

XMLConfigBuilde

XMLConfigBuilderparse方法会返回一个Configuration实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 解析 XML 成 Configuration 对象。
*
* @return Configuration 对象
*/
public Configuration parse() {
// 若已解析,抛出 BuilderException 异常
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 标记已解析
parsed = true;
// 解析 XML configuration 节点
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}

对于配置文件的解析全部体现在parseConfiguration(parser.evalNode("/configuration"));,进入到该方法中可以看到,这个方法做的事情就是去一一解析Document对象中的标签,然后将解析后的标签值设置到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 {
//issue #117 read properties first
// 解析 <properties /> 标签
propertiesElement(root.evalNode("properties"));
// 解析 <settings /> 标签
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 加载自定义的 VFS 实现类
loadCustomVfs(settings);
// 解析 <typeAliases /> 标签
typeAliasesElement(root.evalNode("typeAliases"));
// 解析 <plugins /> 标签
pluginElement(root.evalNode("plugins"));
// 解析 <objectFactory /> 标签
objectFactoryElement(root.evalNode("objectFactory"));
// 解析 <objectWrapperFactory /> 标签
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 解析 <reflectorFactory /> 标签
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 赋值 <settings /> 到 Configuration 属性
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 解析 <environments /> 标签
environmentsElement(root.evalNode("environments"));
// 解析 <databaseIdProvider /> 标签
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 解析 <typeHandlers /> 标签
typeHandlerElement(root.evalNode("typeHandlers"));
// 解析 <mappers /> 标签
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}

由于配置项过多,我们这对其中几个比较常用的配置解析进行说明。

解析properties标签

1
2
3
4
5
6
7
8
9
10
11
<properties resource="org/mybatis/example/config.properties">
<property name="username" value="dev_user"/>
<property name="password" value="F2Fa3!33TYyg"/>
</properties>

<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>

properties的作用简单说就是去动态替换属性的值。properties基本和Java语言中的Properties是一个意思,其本质就体现Configuration中的variables字段。

1
2
3
4
5
6
/**
* 变量 Properties 对象。
*
* 参见 {@link org.apache.ibatis.builder.xml.XMLConfigBuilder#propertiesElement(XNode context)} 方法
*/
protected Properties variables = new Properties();

细心一点就会发现,其实Properties不仅可以在XML配置中定义,而且还可以通过读取Properties方法,以及通过XMLConfigBuilder(reader, environment, properties)的方式传入到variables中。

  • XML配置中的Properties
  • 应用中的Properties文件
  • 作为XMLConfigBuilder构造方法参数传递的Properties
    当以上3个地方都有相同名字的Properties时,那么MyBatis会用哪一个呢?对于这一点,文档中有特别解释。

如果属性在不只一个地方进行了配置,那么 MyBatis 将按照下面的顺序来加载:
properties 元素体内指定的属性首先被读取。
然后根据 properties 元素中的 resource 属性读取类路径下属性文件或根据 url 属性指定的路径读取属性文件,并覆盖已读取的同名属性。
最后读取作为方法参数传递的属性,并覆盖已读取的同名属性。
因此,通过方法参数传递的属性具有最高优先级,resource/url 属性中指定的配置文件次之,最低优先级的是 properties 属性中指定的属性。

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
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 读取XML子标签们,为 Properties 对象
Properties defaults = context.getChildrenAsProperties();
// 读取Properties文件 resource 和 url 属性
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) { // resource 和 url 都存在的情况下,抛出 BuilderException 异常
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
// 读取本地 Properties 配置文件到 defaults 中。
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
// 读取远程 Properties 配置文件到 defaults 中。
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
// 作为XMLConfigBuilder构造方法参数传递的Properties
// 覆盖 configuration 中的 Properties 对象到 defaults 中。
Properties vars = configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 设置 defaults 到 parser 和 configuration 中。
parser.setVariables(defaults);
configuration.setVariables(defaults);
}
}

以上做了两件事:

  • 依次分别读取XML配置的Properties标签、读取本地文件系统或远程网络的Properties配置文件(取决于<properties>节点的 resourceurl 是否为空)、作为XMLConfigBuilder构造方法的Properties参数,将其全部putAlldefaults中。
  • 设置 defaultsparserconfiguration
    所以,优先级最高的是作为XMLConfigBuilder构造方法的Properties参数、其次是本地文件系统或远程网络的Properties配置文件、优先级最低的是XML配置的Properties标签。

解析settings标签

settings是对MyBatis 行为和属性的定义,比如useGeneratedKeys这个setting代表的含义就是允许 JDBC 支持自动生成主键。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Properties settingsAsProperties(XNode context) {
// 将子标签,解析成 Properties 对象
if (context == null) {
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// Check that all settings are known to the configuration class
// 校验每个属性,在 Configuration 中,有相应的 setting 方法,否则抛出 BuilderException 异常
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
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;
}

以上做了两件事:

  • 解析setting标签到props
  • 反射Configuration,循环props,调用metaConfig.hasSetter方法检测Configuration中是否持有对应的setter方法,否则抛出异常。

为什么需要这样做,我的理解是在对settings设置到Configuration前端,需要先进行安全检查,从而保证用于填写的settings标签都是正确的。

解析typeAliases标签

typeAliases中文直译就是「类型别名」,解决的问题在于简化类的全限名的冗余。

1
2
3
4
5
6
7
8
9
<typeAliases>
<typeAlias alias="Author" type="domain.blog.Author"/>
<typeAlias alias="Blog" type="domain.blog.Blog"/>
<typeAlias alias="Comment" type="domain.blog.Comment"/>
<typeAlias alias="Post" type="domain.blog.Post"/>
<typeAlias alias="Section" type="domain.blog.Section"/>
<typeAlias alias="Tag" type="domain.blog.Tag"/>
<package name="domain.blog"/>
</typeAliases>

现在当前的XML文件中,如果之后在resultType中要用到domain.blog.Author类型引用,就只要用Author就可以代替。在typeAliases标签中支持两种注册别名的方式:

  • 准确注册:typeAlias标签,定义alias为别名,type为别名引用,用于为准确的一个类注册别名;
  • 包注册:package标签,name属性,将整个包中的类都注册为别名,除非类中已经声明@Alias注解,否则别名默认为类的小写。
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
private void typeAliasesElement(XNode parent) {
if (parent != null) {
// 遍历子节点
for (XNode child : parent.getChildren()) {
// 指定为包的情况下,注册包下的每个类
if ("package".equals(child.getName())) {
String typeAliasPackage = child.getStringAttribute("name");
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
// 指定为类的情况下,直接注册类和别名
} else {
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class<?> clazz = Resources.classForName(type); // 获得类是否存在
// 注册到 typeAliasRegistry 中
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) { // 若类不存在,则抛出 BuilderException 异常
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}

以上做了3件事:

  • 遍历typeAlias标签
  • 匹配到package时,将整个包的类都注册为别名
  • 否则直接注册类和别名,注册之前需要先检查一下类是否存在,不存在就跑出异常
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
public class TypeAliasRegistry {

/**
* 类型与别名的映射。
*/
private final Map<String, Class<?>> TYPE_ALIASES = new HashMap<>();

/**
* 初始化默认的类型与别名
*
* 另外,在 {@link org.apache.ibatis.session.Configuration} 构造方法中,也有默认的注册
*/
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);
...
...
}
}

可以看到,注册到MyBatis中的别名和类都存在一个叫typeAliasRegistry字段中,打开TypeAliasRegistry这个类,发现其持有一个以StringkeyClass<?>valuehashmap,而在构造方法中也初始化了一系列的JDK原生的对象类型,这也解释了我们不用自己动手再去注册这个基础对象类型。在注释中,说到在Configuration的构造方法中也有类似的注册,其中注册大多都是业务对象的别名。

1
2
3
4
5
6
7
public Configuration() {
// 注册到 typeAliasRegistry 中 begin ~~~~
typeAliasRegistry.registerAlias("JDBC", JdbcTransactionFactory.class);
typeAliasRegistry.registerAlias("MANAGED", ManagedTransactionFactory.class);
...
...
}

这里还有个问题没解决,MyBatis时怎么处理没有声明alias这种情况的呢?比如下面这种场景

1
2
3
<typeAliases>
<typeAlias type="domain.blog.Author"/>
</typeAliases>

点进TypeAliasRegistry#registerAlias方法则解释了对该情况的处理

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
//alias为null的情况
public void registerAlias(Class<?> type) {
// 默认为,简单类名,获取全路径类名的简称,比如, 全限定类名 xyz.coolblog.model.Author 的别名为 author。
String alias = type.getSimpleName();
// 如果有注解,使用注册上的名字
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
// 注册类型与别名的注册表
registerAlias(alias, type);
}

public void registerAlias(String alias, Class<?> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
// 转换成小写
String key = alias.toLowerCase(Locale.ENGLISH);
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) { // 冲突,抛出 TypeException 异常
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
TYPE_ALIASES.put(key, value);
}

MyBatis对于aliasnull的情况的处理方式是直接获取注册的这个类的SimpleName,之后再检查这个类中是否存在alias注解,有的话直接用声明的注解中的值,注册到typeAliasRegistry之前需要将alias全部小写。

解析environments标签

一般而言,我们通常在配置文件中定义environment

1
2
3
4
5
6
7
8
9
10
11
12
13
<environments default="development">
<environment id="development">
<transactionManager type="JDBC">
<property name="..." value="..."/>
</transactionManager>
<dataSource type="POOLED">
<property name="driver" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
</dataSource>
</environment>
</environments>

environments的中文解释是「环境配置」,在MyBatis的文档中特别强调了,SqlSessionFactoryenvironment是一对一关系,也就是说,对于一个数据库环境比如MySQL环境,需要一个SqlSessionFactory实例,但如果这时需要增加一个数据库环境(比如Oracle环境或者另外一台MySQL主机环境),则需要再添加一个environment,再添加一个SqlSessionFactory实例。所以不会出现说,一个SqlSessionFactory实例同时选择多个environment的情况。

尽管可以配置多个环境,但每个 SqlSessionFactory 实例只能选择一种环境。
每个数据库对应一个 SqlSessionFactory 实例

这样就解释了,在创建SqlSessionFactory实例时,可以通过直接将environment作为入参的方式。

1
2
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment);
SqlSessionFactory factory = new SqlSessionFactoryBuilder().build(reader, environment, properties);

environmentMyBatis 中便是org.apache.ibatis.mapping.Environment这个类,environmentsElement方法做的事情就是,先找出default环境,遍历environments节点找出对应的环境配置,然后初始化TransactionFactoryDataSourceFactoryDataSource 这些实例,最后再构造出该环境并设置到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
private void environmentsElement(XNode context) throws Exception {
if (context != null) {
// environment 属性非空,从 default 属性获得
if (environment == null) {
environment = context.getStringAttribute("default");
}
// 遍历 XNode 节点
for (XNode child : context.getChildren()) {
// 判断 environment 是否匹配
String id = child.getStringAttribute("id");
if (isSpecifiedEnvironment(id)) {
// 解析 `<transactionManager />` 标签,返回 TransactionFactory 对象
TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager"));
// 解析 `<dataSource />` 标签,返回 DataSourceFactory 对象
DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource"));
DataSource dataSource = dsFactory.getDataSource();
// 创建 Environment.Builder 对象
Environment.Builder environmentBuilder = new Environment.Builder(id)
.transactionFactory(txFactory)
.dataSource(dataSource);
// 构造 Environment 对象,并设置到 configuration 中
configuration.setEnvironment(environmentBuilder.build());
}
}
}
}

解析mappers标签

MyBatis 中有两种配置文件:

  • 全局配置的Configuration文件
  • 用于记录SQL语句的Mapper文件

Configuration 文件中,mappers标签的用处就是去声明具体执行的SQL语句记录在哪里。mappers标签支持4种声明方式:

  1. 使用相对于类路径的资源引用:<mapper resource="abc.xml"/>
  2. 使用完全限定资源定位符(URL):<mapper url="file:///abc.xml"/>
  3. 使用映射器接口实现类的完全限定类名:<mapperclass="abcMapper"/>
  4. 将包内的映射器接口实现全部注册为映射器:<package name="org.mybatis.builder"/>

我的理解以上的声明方式可以分为两类:一类是引用XML文件,一类是引用Mapper Interface

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
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
// 遍历子节点
for (XNode child : parent.getChildren()) {
// 如果是 package 标签,则扫描该包
if ("package".equals(child.getName())) {
// 获得包名
String mapperPackage = child.getStringAttribute("name");
// 添加到 configuration 中
configuration.addMappers(mapperPackage);
// 如果是 mapper 标签,
} else {
// 获得 resource、url、class 属性
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
// 使用相对于类路径的资源引用
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
// 获得 resource 的 InputStream 对象
InputStream inputStream = Resources.getResourceAsStream(resource);
// 创建 XMLMapperBuilder 对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
// 执行解析
mapperParser.parse();
// 使用完全限定资源定位符(URL)
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
// 获得 url 的 InputStream 对象
InputStream inputStream = Resources.getUrlAsStream(url);
// 创建 XMLMapperBuilder 对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
// 执行解析
mapperParser.parse();
// 使用映射器接口实现类的完全限定类名
} else if (resource == null && url == null && mapperClass != null) {
// 获得 Mapper 接口
Class<?> mapperInterface = Resources.classForName(mapperClass);
// 添加到 configuration 中
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}

MyBatis解析mappers标签时,会便利每一个mapper的节点,首先区分是否为package,若属性不为package则检测其他3种情况,根据每种情况再具体处理。
可以跟进package的代码往下看。

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
public <T> void addMapper(Class<T> type) {
// 判断,必须是接口。
if (type.isInterface()) {
// 已经添加过,则抛出 BindingException 异常
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
// 添加到 knownMappers 中
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.
// 解析 Mapper 的注解配置
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
// 标记加载完成
loadCompleted = true;
} finally {
// 若加载未完成,从 knownMappers 中移除
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}

以上做的事情时将package下的interface全部注册到MapperRegistry中,在MapperRegistry中持有knownMappers去存储这些interface的信息。
-w889
到现在为止,只是处理了interface,那么真实映射的那些XML配置文件时在哪里处理的呢。可以跟进到parser.parse();中。

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
public void parse() {
// 判断当前 Mapper 接口是否应加载过。
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
// 加载对应的 XML Mapper 文件
loadXmlResource();
// 标记该 Mapper 接口已经加载过
configuration.addLoadedResource(resource);
// 设置 namespace 属性
assistant.setCurrentNamespace(type.getName());
// 解析 @CacheNamespace 注解
parseCache();
// 解析 @CacheNamespaceRef 注解
parseCacheRef();
// 遍历每个方法,解析其上的注解
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
// 执行解析
parseStatement(method);
}
} catch (IncompleteElementException e) {
// 解析失败,添加到 configuration 中
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
// 解析待定的方法
parsePendingMethods();
}

loadXmlResource() 做的事情主要是对于Mapper XML文件的处理,具体怎么解析在下一篇文章中介绍。loadedResources用来存储已加载资源的信息。
-w917
最后解析后的语句信息保存在mappedStatements中,mappedStatements是一个HashMapkeynamespace.id 的字符,具体执行的SQL语句信息就是value,在实际debug中,发现同时也存储了相同一份为idEntry。不知道MyBatis为什么要这样做。
-w917

总结

回过头来,我们再看如何去生成一个SqlSessionFactory,从SqlSessionFactoryBuilderbuild方法中我们可以找到答案。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
// 创建 XMLConfigBuilder 对象
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 执行 XML 解析
// 创建 DefaultSqlSessionFactory 对象
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}

build方法做的事情就是读取配置文件XML转换为Configuration实例。其中最关键的两句代码代表来这个过程的两个阶段。

-w915

  1. 创建 XMLConfigBuilder 对象,就是读取全局配置XML文件到DOM对象的过程,这个过程中包含对XML配置的校验。
  2. DOM对象到Configuration对象的过程,解析全局配置XML各个标签,以及解析mapper标签中声明的mapper.xml文件。

参考引用

MyBatis 文档