您当前的位置:首页 > 计算机 > 编程开发 > MyBatis

Chapter2 MyBatis 配置文件解析过程

时间:12-14来源:作者:点击数:

本章对 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> 节点

<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>节点

<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 方法等。关于这个类的逻辑,待会我会详细解析。接下来,简单总结一下上面代码的逻辑。

  1. 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
  2. 为 Configuration 创建元信息对象
  3. 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法,不存在则抛异常
  4. 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束

下面,我们重点关注一下第 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);
    }
	// 省略其他方法
}
方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐