本章对 MyBatis 解析配置文件的过程进行分析。
我们在使用 MyBatis 时,第一步要做的事情一般是根据配置文件构建 SqlSessionFactory对象。 相关代码大致如下:
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
在上面代码中, 我们首先会使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的 build 方法构建 SqlSessionFactory对象。 这里的 build 方法是我们分析配置文件解析过程的入口方法。 下面我们来看一下这个方法的代码:
// org.apache.ibatis.session.SqlSessionFactoryBuilder.java
public SqlSessionFactory build(InputStream inputStream) {
return this.build((InputStream)inputStream, (String)null, (Properties)null);
}
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
SqlSessionFactory var5;
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
var5 = this.build(parser.parse());
} catch (Exception var14) {
throw ExceptionFactory.wrapException("Error building SqlSession.", var14);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException var13) {
}
}
return var5;
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
从上面的代码中,我们大致可以猜出 MyBatis 配置文件是通过 XMLConfigBuilder 进行解析的。不过目前这里还没有非常明确的解析逻辑,所以我们继续往下看。这次来看一下 XMLConfigBuilder 的 parse 方法,如下:
public Configuration parse() {
if (this.parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
} else {
this.parsed = true;
this.parseConfiguration(this.parser.evalNode("/configuration"));
return this.configuration;
}
}
private void parseConfiguration(XNode root) {
try {
this.propertiesElement(root.evalNode("properties"));
Properties settings = this.settingsAsProperties(root.evalNode("settings"));
this.loadCustomVfs(settings);
this.loadCustomLogImpl(settings);
this.typeAliasesElement(root.evalNode("typeAliases"));
this.pluginElement(root.evalNode("plugins"));
this.objectFactoryElement(root.evalNode("objectFactory"));
this.objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
this.reflectorFactoryElement(root.evalNode("reflectorFactory"));
this.settingsElement(settings);
this.environmentsElement(root.evalNode("environments"));
this.databaseIdProviderElement(root.evalNode("databaseIdProvider"));
this.typeHandlerElement(root.evalNode("typeHandlers"));
this.mapperElement(root.evalNode("mappers"));
} catch (Exception var3) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + var3, var3);
}
}
从上面的代码中,首先找到了 XPath 为 /configuration 的节点,并对改节点的子节点进行解析,包括 properties,settings,typeAliases 等等,每一个子节点都有一个对应的解析方法。接下来,我们就来看看几个常用的节点解析的逻辑。
<properties> 节点的解析工作由 propertiesElement 这个方法完成的, 在分析方法的源码前,我们先来看一下 <properties> 节点是如何配置的。如下:
<properties resource="jdbc.properties">
<property name="jdbc.username" value="coolblog"/>
<property name="hello" value="world"/>
</properties>
该 xml 中的 properties 节点包括了一个 resource 属性、两个子节点,我们参考这个配置,来分析 propertiesElement 方法的逻辑:
// XMLConfigBuilder.java
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");
// resource和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放入Properties对象中
if (resource != null) {
// 解析指定路径的属性文件
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// 解析指定url的属性文件
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = this.configuration.getVariables();
if (vars != null) {
defaults.putAll(vars);
}
// 将解析的值设置到XPathParser和configuration中
this.parser.setVariables(defaults);
this.configuration.setVariables(defaults);
}
}
// XNode.java
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
Iterator var2 = this.getChildren().iterator();
// 获取所有 property 节点的 name 和 value 属性
while(var2.hasNext()) {
XNode child = (XNode)var2.next();
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
}
// XNode.java
public List<XNode> getChildren() {
// 将链表格式的子节点数据转换为List格式,并返回
List<XNode> children = new ArrayList();
NodeList nodeList = this.node.getChildNodes();
if (nodeList != null) {
int i = 0;
for(int n = nodeList.getLength(); i < n; ++i) {
Node node = nodeList.item(i);
if (node.getNodeType() == 1) {
children.add(new XNode(this.xpathParser, node, this.variables));
}
}
}
return children;
}
从代码可知,首先解析 properties 的子节点和属性信息,子节点解析结果赋值给 defaults 对象;然后解析 resource 或 url 属性信息对应的文件,并将解析结果放到 defaults 对象(**属性对应文件如果和子节点出现相同 name,那么属性中相同 name 的值会覆盖相应子节点的值,这会导致同名属性覆盖的问题 **);将解析结果 defaults 对象赋值给 parser 和 configuration。
<settings> 相关配置是 MyBatis 中非常重要的配置,这些配置用于调整 MyBatis 运行时的行为。 settings 配置繁多,在对这些配置不熟悉的情况下,保持默认配置即可。关于 <settings> 相关配置, MyBatis 官网上进行了比较详细的描述,大家可以去了解一下。在本节中,暂时还用不到这些配置,所以即使不了解这些配置也没什么关系。下面先来看一个比较简单的配置,如下:
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="autoMappingBehavior" value="PARTIAL"/>
</settings>
接下来,对照上面的配置,来分析相关源码
// XMLConfigBuilder
private Properties settingsAsProperties(XNode context) {
if (context == null) {
return new Properties();
} else {
// 获取 settings 子节点中的内容
Properties props = context.getChildrenAsProperties();
// 创建 Configuration 类的“元信息”对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class, this.localReflectorFactory);
Iterator var4 = props.keySet().iterator();
// 检测 Configuration 中是否存在相关属性,不存在则抛出异常
Object key;
do {
if (!var4.hasNext()) {
return props;
}
key = var4.next();
} while(metaConfig.hasSetter(String.valueOf(key)));
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
如上, settingsAsProperties 方法看起来并不复杂,不过这是一个假象。在上面的代码中出现了一个陌生的类 MetaClass,这个类是用来做什么的呢?答案是用来解析目标类的一些元信息,比如类的成员变量, getter/setter 方法等。关于这个类的逻辑,待会我会详细解析。接下来,简单总结一下上面代码的逻辑。
下面,我们重点关注一下第 2 步和第 3 步的流程。这两步流程对应的代码较为复杂,需要一点耐心阅读
元信息对象创建过程
元信息类MetaClass的构造方法为私有类型,所以不能直接创建,必须使用其提供的forClass 方法进行创建。它的创建逻辑如下:
public class MetaClass {
private final ReflectorFactory reflectorFactory;
private final Reflector reflector;
private MetaClass(Class<?> type, ReflectorFactory reflectorFactory) {
this.reflectorFactory = reflectorFactory;
this.reflector = reflectorFactory.findForClass(type);
}
public static MetaClass forClass(Class<?> type, ReflectorFactory reflectorFactory) {
return new MetaClass(type, reflectorFactory);
}
// 省略其他方法
}
