本章对 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);
- }
- // 省略其他方法
- }