MyBatis3源码深度解析(十四)SqlSession的创建与执行(一)Configuration与SqlSession的创建过程

第五章 SqlSssion的创建过程

前言

MyBatis的核心组件之一SqlSession对象,表示框架与数据库建立的会话,通过该对象的实例可以完成对数据库的增删改查操作。

SqlSession对象的创建过程可以拆解为3个阶段:Configuration实例的创建过程、SqlSessionFactory实例的创建过程和SqlSession实例化的过程。

5.1 XPath方法解析XML文件

MyBatis的主配置文件和Mapper配置文件都是使用的是XML格式。

MyBatis框架在启动时,会解析这些XML配置,将配置信息转换并注册到Configuration组件中。

JDK API提供了3种方式解析XML,分别为DOM、SAX和XPath。这3种方式各有特点,MyBatis选用的方式是XPath

因此,在研究Configuration组件的创建过程之前,有必要研究一下如何通过XPath解析XML文件。

5.1.1 XPath的基本用法

首先创建一个XML文件users.xml:

<?xml version="1.0" encoding="UTF-8"?>

<users>
    <user id="1">
        <name>孙悟空</name>
        <age>1500</age>
        <phone>18955245635</phone>
        <birthday>0000-01-01</birthday>
    </user>
    <user id="2">
        <name>猪八戒</name>
        <age>1000</age>
        <phone>14577898652</phone>
        <birthday>0600-10-01</birthday>
    </user>
</users>

该XML文件中定义的用户信息与Java实体类User的属性是一一对应的:

public class User {
    private Integer id;
    private String name;
    private Integer age;
    private String phone;
    private Date birthday;
    // constructor getter setter ...
}

然后编写测试代码:

示例1

@Test
public void testXPath() throws IOException, ParserConfigurationException, SAXException, XPathExpressionException, ParseException {
    // (1)创建表示XML文档的Document对象
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    DocumentBuilder builder = factory.newDocumentBuilder();
    InputStream inputStream = Resources.getResourceAsStream("users.xml");
    Document document = builder.parse(inputStream);
    // (2)创建用于执行XPath表达式的XPath对象
    XPath xPath = XPathFactory.newInstance().newXPath();
    // (3)使用XPath对象执行表达式,获取XML内容
    NodeList nodeList = (NodeList) xPath.evaluate("/users/*", document, XPathConstants.NODESET);
    List<User> userList = new ArrayList<>();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    for (int i = 1; i < nodeList.getLength() + 1; i++) {
        String path = "/users/user[" + i + "]";
        String idStr = (String) xPath.evaluate(path + "/@id", document, XPathConstants.STRING);
        String name = (String) xPath.evaluate(path + "/name", document, XPathConstants.STRING);
        String ageStr = (String) xPath.evaluate(path + "/age", document, XPathConstants.STRING);
        String phone = (String) xPath.evaluate(path + "/phone", document, XPathConstants.STRING);
        String birthdayStr = (String) xPath.evaluate(path + "/birthday", document, XPathConstants.STRING);
        User user = new User(Integer.valueOf(idStr), name, Integer.valueOf(ageStr), phone, sdf.parse(birthdayStr));
        userList.add(user);
    }
    userList.forEach(System.out::println);
}

控制台打印执行结果:

User{id=1, name='孙悟空', age=1500, phone='18955245635', birthday=Thu Jan 01 00:00:00 CST 1}
User{id=2, name='猪八戒', age=1000, phone='14577898652', birthday=Sat Oct 01 00:00:00 CST 600}

由 示例1 可知,使用JDK提供的XPath解析XML文件的过程大致如下:

(1)创建表示XML文档的Document对象

无论通过哪种方式解析XML文件,都需要先创建表示XML文档的Document对象。

Document对象的创建依赖于DocumentBuilder对象,而DocumentBuilder对象采用工厂模式创建,因此首先需要调用DocumentBuilderFactory类的newInstance()方法创建一个工厂类,然后调用工厂类的newDocumentBuilder()方法创建DocumentBuilder对象,再调用DocumentBuilder对象的parse()方法创建Document对象。parse()方法需要XML文件的输入流作为参数。

(2)创建用于执行XPath表达式的XPath对象

XPath对象也采用工厂模式创建,因此首先要调用XPathFactory类的newInstance()方法创建一个工厂类,然后调用工厂类的newXPath()方法创建一个XPath对象。

(3)使用XPath对象执行表达式,获取XML内容

XPath表达式是一种在XML文档中用于定位节点的语言。它基于XML的树状结构,允许通过路径表达式来选取节点。XPath表达式可以组合使用,以实现更复杂的查找和选择。

XPath表达式的基本结构如下:
1. 节点选择。使用“/”符号从根节点开始选择,例如“/catalog/cd/price”会选择文档中根节点下的“catalog”子节点下的“cd”子节点下的“price”元素。(简单讲,就是一路往下找)
2. 相对和绝对路径。使用“//”符号选择文档中在任何位置匹配上的节点,例如“//book[@id=‘chinese’]”会选择文档中所有ID为“chinese”的“book”元素。
3. 选取当前节点。使用“.”符号选择当前操作的节点,例如“./childnode”会选择当前节点下的“childnode”子节点。
4. 选取父节点。使用“…”符号选择当前节点的父节点,例如“…/childnode”会选择当前节点的父节点下的“childnode”子节点。
5. 选取属性。使用“@”符号选择节点的属性,例如“@id”会选择元素标签上的ID属性。
6. 选取文本。使用“text()”函数选择节点的文本内容,例如“text()=‘chinese’”会选择所有文本内容为“chinese”的节点。
7. 谓语。放在方括号中的谓语用来筛选节点,例如“//book[price>35]”会选择所有价格大于35的“book”元素。
8. 通配符。使用“”符号选择所有匹配的节点,例如“//[@id]”会选择所有ID属性不为空的节点。
9. 命名空间。使用“@”符号和命名空间前缀来指定节点的命名空间,例如“@xmlns:a=‘http://www.example.com/a’”会选择所有命名空间前缀为“a”的节点。

在 示例1 中,首先执行的XPath表达式是"/users/*",它的执行结果是一个NodeList对象,表示“users”节点下的所有节点,即2个“user”节点,因此for循环中nodeList.getLength()的结果为2。

在for循环中,首先执行的XPath表达式是/users/user[i]/@id,表示获取“user”节点的id属性。然后依次执行的XPath表达式是/users/user[i]/name/users/user[i]/age/users/user[i]/phone/users/user[i]/birthday,分别表示获取“user”节点的“name”、“age”、“phone”、“birthday”元素。

需要注意的是,XPath对象的evaluate()方法的最后一个由XPathConstants类指定的参数,用于设置XPath表达式要返回的值的类型。

源码1javax.xml.xpath.XPathConstants

public class XPathConstants {
    private XPathConstants() { }
    public static final QName NUMBER = new QName("http://www.w3.org/1999/XSL/Transform", "NUMBER");
    public static final QName STRING = new QName("http://www.w3.org/1999/XSL/Transform", "STRING");
    public static final QName BOOLEAN = new QName("http://www.w3.org/1999/XSL/Transform", "BOOLEAN");
    public static final QName NODESET = new QName("http://www.w3.org/1999/XSL/Transform", "NODESET");
    public static final QName NODE = new QName("http://www.w3.org/1999/XSL/Transform", "NODE");
    public static final String DOM_OBJECT_MODEL = "http://java.sun.com/jaxp/xpath/dom";
}

由 源码1 可知,XPath表达式的执行结果为XML节点对象(Node、NodeLis等)或者字符串、数值类型、布尔类型等。

5.1.2 MyBatis使用XPathParser工具类

MyBatis为了简化XPath的操作,提供了一个XPathParser工具类封装了对XML的解析操作,同时使用XNode类增强了对XML节点的操作。

因此,使用XPathParser工具类,可以将上面的示例代码修改成如下所示:

示例2

@Test
public void testXPathParser() throws IOException {
    InputStream inputStream = Resources.getResourceAsStream("users.xml");
    // (1)创建XPathParser工具类
    XPathParser parser = new XPathParser(inputStream);
    // (2)调用evalNodes()方法获取所有符合表达式的节
    List<XNode> nodeList = parser.evalNodes("/users/*");
    for (XNode node : nodeList) {
        System.out.println("--------- ");
        // (3)调用getLongAttribute方法获取节点的属性
        Long id = node.getLongAttribute("id");
        System.out.println("id = " + id);
        List<XNode> children = node.getChildren();
        for (XNode childNode : children) {
            // (4)调用getName和getStringBody方法获取节点的名称和值
            String name = childNode.getName();
            String stringBody = childNode.getStringBody();
            System.out.println(name + " = " + stringBody);
        }
    }
}

由 示例2 可知,使用XPathParser工具类后,解析XML的逻辑变得更加简便。通过调用XPathParser工具类的evalNodes()方法即可获取所有符合表达式的节点,而获取节点的属性只需调用XNode对象的getLongAttribute()方法,获取节点名称调用getName()方法,获取节点值调用getStringBody()方法。

打开XPathParser工具类的构造方法的源码,可以发现它也是先创建DocumentBuilderFactory对象,再创建DocumentBuilder对象,最后调用DocumentBuilder的parse()方法创建一个Document对象。这与 示例1 的写法是一致的。

5.2 Configuration实例创建过程

Configuration组件是MyBatis非常重要的核心组件之一,主要有以下3个作用:

(1)用于描述MyBatis配置信息,包括主配置文件mybatis-config.xml和Mapper配置文件;
(2)作为容器注册MyBatis的其他组件,例如TypeHandler、MappedStatement等;
(3)提供工厂方法,创建ResultSetHandler、StatementHandler、Executor、ParameterHandler等组件。

【MyBatis3源码深度解析(十二)MyBatis的核心组件(一)Configuration】中编写的示例项目中,有这样一段代码:

示例3

// 读取配置文件
Reader reader = Resources.getResourceAsReader("mybatis-config.xml");
// 创建SqlSession
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);
SqlSession sqlSession = sqlSessionFactory.openSession();

由 示例3 可知,在SqlSession实例化之前,MyBatis会通过SqlSessionFactory的build()方法解析主配置文件及所有Mapper配置文件,创建一个Configuration对象。

源码2org.apache.ibatis.session.SqlSessionFactoryBuilder

public SqlSessionFactory build(Reader reader) {
    return build(reader, null, null);
}

public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
    try {
        // 创建一个XMLConfigBuilder对象
        XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
        // 调用parse()方法创建Configuration实例
        return build(parser.parse());
    } // catch finally ...
}

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

由 源码2 可知,在build()方法中会创建一个XMLConfigBuilder对象,该类的parse()方法会返回一个Configuration实例。

源码3org.apache.ibatis.builder.xml.XMLConfigBuilder

public class XMLConfigBuilder extends BaseBuilder {
    private final XPathParser parser;
    public Configuration parse() {
        // 防止parse()方法被同一个实例调用多次
        if (parsed) {
            throw new BuilderException("Each XMLConfigBuilder can only be used once.");
        }
        parsed = true;
        // 调用XPathParser的evalNode()方法创建表示configuration节点的XNode对象
        // parseConfiguration()方法对XNode对象进行处理
        parseConfiguration(parser.evalNode("/configuration"));
        return configuration;
    }
    // ......
}

由 源码3 可知,parse()方法首先调用XPathParser工具类的evalNode()方法获取XML配置文件中<configuration>节点对应的XNode对象,接着调用parseConfiguration()方法通过该XNode对象获取更多配置信息。

源码4org.apache.ibatis.builder.xml.XMLConfigBuilder

private void parseConfiguration(XNode root) {
    try {
        // issue #117 read properties first
        // 处理<properties>标签
        propertiesElement(root.evalNode("properties"));
        // 处理<settings>标签
        Properties settings = settingsAsProperties(root.evalNode("settings"));
        loadCustomVfsImpl(settings);
        loadCustomLogImpl(settings);
        // 处理<typeAliases>标签
        typeAliasesElement(root.evalNode("typeAliases"));
        // 处理<plugins>标签
        pluginsElement(root.evalNode("plugins"));
        // 处理<objectFactory>标签
        objectFactoryElement(root.evalNode("objectFactory"));
        // 处理<objectWrapperFactory>标签
        objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
        // 处理<reflectorFactory>标签
        reflectorFactoryElement(root.evalNode("reflectorFactory"));
        settingsElement(settings);
        // read it after objectFactory and objectWrapperFactory issue #631
        // 处理<environments>标签
        environmentsElement(root.evalNode("environments"));
        // 处理<databaseIdProvider>标签
        databaseIdProviderElement(root.evalNode("databaseIdProvider"));
        // 处理<typeHandlers>标签
        typeHandlersElement(root.evalNode("typeHandlers"));
        // 处理<mappers>标签
        mappersElement(root.evalNode("mappers"));
    } catch (Exception e) {
        throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
    }
}

由 源码4 可知,在parseConfiguration()方法中,对于<configuration>标签的子标签,都有一个单独的处理方法,例如propertiesElement()方法处理<properties>标签,settingsAsProperties()方法处理<settings>标签,其它如注释所示。

这些标签具体有什么作用,参考【MyBatis3源码深度解析(十二)MyBatis的核心组件(一)Configuration】

每个标签的解析细节,可以以处理<mappers>标签为例子来研究。

假设主配置文件mybatis-config.xml中有以下配置:

<mappers>
    <!--方式一:通过指定XML文件的类路径来注册-->
    <mapper resource="mapper/UserMapper.xml"/>
    <!--方式二:通过指定XML文件的完全限定资源定位符来注册-->
    <mapper url="file:///C:\workspace\mybatis_demo2\src\main\resources\mapper\UserMapper.xml"/>
    <!--方式三:通过Mapper接口的类路径来注册-->
    <mapper class="com.star.mybatis.mapper.UserMapper"/>
    <!--方式四:通过Mapper接口所在包路径类注册-->
    <package name="com.star.mybatis.mapper"/>
</mappers>
源码5org.apache.ibatis.builder.xml.XMLConfigBuilder

private void mappersElement(XNode context) throws Exception {
    // 传入的参数是<mappers>标签对应的XNode对象
    if (context == null) {
        return;
    }
    // 遍历<mappers>标签的子标签
    for (XNode child : context.getChildren()) {
        if ("package".equals(child.getName())) {
            // 如果子标签是<package>,则说明要根据包名来扫描(即方式四)
            // 则将<package>标签的name属性提取出来
            // addMappers方法会根据包名使用反射机制提取出包下的所有Mapper接口
            String mapperPackage = child.getStringAttribute("name");
            configuration.addMappers(mapperPackage);
        } else {
            // 如果子标签是<mapper>,则提取<mapper>标签的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) {
                // resource属性不为空,url、class属性属性为空 -> 方式一
                // 使用XMLMapperBuilder加载Mapper配置文件
                ErrorContext.instance().resource(resource);
                try (InputStream inputStream = Resources.getResourceAsStream(resource)) {
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
                            configuration.getSqlFragments());
                    mapperParser.parse();
                }
            } else if (resource == null && url != null && mapperClass == null) {
                // url属性不为空,resource、class属性属性为空 -> 方式二
                // 使用XMLMapperBuilder加载Mapper配置文件
                ErrorContext.instance().resource(url);
                try (InputStream inputStream = Resources.getUrlAsStream(url)) {
                    XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url,
                            configuration.getSqlFragments());
                    mapperParser.parse();
                }
            } else if (resource == null && url == null && mapperClass != null) {
                // class属性不为空,resource、url属性属性为空 -> 方式三
                // 使用反射机制加载Mapper接口
                Class<?> mapperInterface = Resources.classForName(mapperClass);
                configuration.addMapper(mapperInterface);
            } else {
                // throw ...
        }
    }
}

由 源码5 可知,解析<mappers>标签的mappersElement()针对四种可行的配置方式分别进行了处理,最终将Mapper配置文件或Mapper接口注册到Configuration组件中。其他标签的处理方法的逻辑与这相似,不再赘述。

5.3 SqlSession实例创建过程

由 示例3 可知,SqlSession实例使用工厂模式创建,因此在创建SqlSession实例之前要创建SqlSessionFactory对象。

SqlSessionFactory对象的创建依赖于SqlSessionFactoryBuilder对象,以主配置文件输入流作为参数调用其build()方法。

由 源码2 可知,build()方法中,首先借助XMLConfigBuilder对象对主配置文件进行解析,生成Configuration对象,然后以Configuration对象为参数,调用重载的build()方法。

源码6org.apache.ibatis.session.SqlSessionFactoryBuilder

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

由 源码6 可知,重载的build方法以Configuration对象为参数,创建了一个SqlSessionFactory对象,具体的落地实现类是DefaultSqlSessionFactory。

SqlSessionFactory创建完毕后,调用其openSession()方法即可创建一个SqlSession实例。

源码7org.apache.ibatis.session.defaults.DefaultSqlSessionFactory

@Override
public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level,
                                             boolean autoCommit) {
    Transaction tx = null;
    try {
        // (1)获取主配置文件中配置的环境信息
        final Environment environment = configuration.getEnvironment();
        // (2)创建事务管理器工厂
        final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
        // (3)创建事务管理器
        tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
        // (4)根据主配置文件中指定的Executor类型创建对应的Executor实例
        final Executor executor = configuration.newExecutor(tx, execType);
        // (5)创建DefaultSqlSession实例
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } // catch finally ......
}

由 源码7 可知,openSession()方法主要有5个步骤,首先通过Configuration对象获取主配置文件中通过<environment>标签配置的环境信息,然后根据该标签的<transactionManager>子标签配置的事务管理器类型创建对应的事务管理器工厂,由该工厂创建对应的事务管理器。

事务管理器创建完毕后,调用Configuration对象的newExecutor()方法,根据主配置文件中<settings>标签下的<setting name="defaultExecutorType" value="..."/>配置指定的Executor类型创建对应的Executor实例,默认类型是SIMPLE。

最后以Configuration对象和Executor对象为参数,创建一个DefaultSqlSession对象。至此,SqlSession实例创建过程结束。

本节完,更多内容请查阅分类专栏:MyBatis3源码深度解析

  • 24
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

灰色孤星A

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值