读取并解析配置文件

2023-10-04 源码探究Mybatis

# 流程步骤分析

如下图所示,我们配合图片分析如何搭建持久层模块,该如何开发?

  1. 读取配置文件:创建Resources类,负责加载配置文件,将配置文件加载成字节输入流,存入内存方便调用。
    • 方法:InputStream getResourcesAsStream(String path) {}
    • 传递的path路径就是sqlMapConfig.xml文件的路径,这个文件存储数据库的配置信息;而Mapper.xml存放SQL语句,这个暂时不管它。
  2. 创建两个JavaBean。
    • Configuration:全局配置类,存储解析出来的SqlMapConfig.xml文件的内容
    • MappedStatement:映射配置类,存储解析mapper,xml文件的内容
  3. 解析配置文件,填充容器对象;
    • 创建SqlSessionFactoryBuilder类,方法SqlSessionFactory build(InputStream in) {}
    • 这个方法干两件事,①解析配置文件到Configuration;②创建DeafultSqlSessionFactory
  4. 创建SqlSession接口及实现类DefaultSqlSessionFactory
    • 方法:SqlSession openSession();
    • 虽然说本质上就是为了拿到SqlSession对象,创建Factory类看着很多余,但是工厂设计模式可以让你传递不同参数,拿到的SqlSession是不同的,后续代码会有详细提现。
  5. 创建SqlSession接口及实现类DefaultSqlSession,方法:selectList、SelectOne
  6. 创建Executor接口及实现类SimpleExecutor,执行底层的JDBC

持久层框架开发思路解析

# 创建配置文件

resources目录下的结构如下所示

|-- resources
    |-- sqlMapConfig.xml
    |-- mapper
        |-- ProductMapper.xml
        |-- UserMapper.xml

想一想配置文件在哪创建?是不是在使用端创建,我们不可能在编写框架的时候就强制定义你的数据库信息,因此肯定是在使用方创建sqlMapConfig.xml文件

<configuration>
    <!--1.配置数据库信息-->
    <dataSource>
        <property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
        <property name="url" value="jdbc:mysql:///test_mybatis?useSSL=false&amp;characterEncoding=UTF-8&amp;serverTimezone=UTC"></property>
        <property name="username" value="root"></property>
        <property name="password" value="root"></property>
    </dataSource>
    <!--2.引入映射配置文件-->
    <mappers>
        <mapper resource="mapper/CustomerMapper.xml"></mapper>
        <mapper resource="mapper/UserMapper.xml"></mapper>
    </mappers>

</configuration>

CustomerMapper.xml暂时为空,UserMapper.xml如下所示:

<mapper namespace="user">
    <select id="selectList" resultType="com.xk857.domain.User">
        select * from user
    </select>
</mapper>

有了id就能确定是哪个方法,为什么还需要再namespace?

id可以确定是哪个方法,但是现在有多个mapper,每个mapper都有findAll,那么此时就无法确定SQL对应的是哪个方法了

# 1.加载配置文件

现在开始框架端编写,首先是加载配置文件。先获取到当前类的类加载器,从而读取到resources目录下的资源文件,资源路径通过参数传递,详细解释如下:

  1. 调用Resources.class.getClassLoader()返回了与Resources类关联的类加载器。
  2. getResourceAsStream()用于获取resources目录下指定路径的文件,返回输入流对象用于读取资源内容。
package com.xk857.io;

public class Resources {

    /**
     * 根据配置文件的路径,加载配置文件成字节输入流,存到内存中
     * @param path resources目录下文件的路径
     * @return InputStream输入流
     */
public class Resources {

    /**
     * 根据配置文件的路径,加载配置文件成字节输入流,存到内存中
     *
     * @param path resources目录下文件的路径
     * @return InputStream输入流
     */
    public static InputStream getResourceAsSteam(String path) {
        try {
            return Resources.class.getClassLoader().getResourceAsStream(path);
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("未查询到配置文件:" + path);
        }
    }
}
在使用端编写测试类。
package com.xk857.io;

public class ResourcesTest {

    @Test
    public void test1() {
        InputStream is = Resources.getResourceAsSteam("sqlMapConfig.xml");
        System.out.println(readInputStream(is));
    }

    // 读取InputStream中的信息
    public static String readInputStream(InputStream inputStream) {
        StringBuilder stringBuilder = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) {
            String line;
            while ((line = reader.readLine()) != null) {
                stringBuilder.append(line).append("\n");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return stringBuilder.toString();
    }
}

# 2.创建两个JavaBean

首先是MappedStatement对象,他存放的是mapper.xml文件中每个标签的信息,比如select标签中的内容,包含SQL语句、返回值类型、参数类型等

package com.xk857.pojo;

/**
 * @author cv大魔王
 * @version 1.0
 * @description 映射配置类,存放解析出来的内容,例如select、update标签中的内容
 * @date 2023/10/4
 */
@Data
public class MappedStatement {

    // 唯一标识,即namespace
    private String statementId;
    
    // 返回值类型
    private String resultType;
    
    // 参数值类型
    private String parameterType;
    
    // sql语句
    private String sql;

  	// 判断当前是什么操作(根据标签设置),比如查询就是select、修改是update
    private String sqlCommandType;
}

然后是Configuration对象,它存储全局的配置信息,也就是数据源相关信息,从sqlMapConfig.xml解析出来的信息将转换成Configuration对象;

  • 数据源的连接地址我们可以存储成数据库、数据库类型、账号密码等;
  • 但JDBC操作时是使用DataSource对象进行操作的,我们直接存储成DataSource对象;
  • 还需要创建一个Map,用于存放MappedStatement对象,也可以理解成存储所有的SQL语句
package com.xk857.pojo;
import javax.sql.DataSource;

/**
 * @author cv大魔王
 * @version 1.0
 * @description 全局配置类:存放核心配置文件解析出来的内容
 * @date 2023/10/4
 */
@Data
public class Configuration {

    // 数据源对象
    private DataSource dataSource;

    //  key:statementId:namespace.id   MappedStatement:封装好的MappedStatement对象
    private Map<String,MappedStatement> mappedStatementMap = new HashMap<>();
}

# 3.解析配置文件

解析XML文件,需要用到如下两个依赖,没用过没关系,注释写的很详细。

<dependencies>
    <!--dom4j 依赖-->
    <dependency>
        <groupId>dom4j</groupId>
        <artifactId>dom4j</artifactId>
        <version>1.6.1</version>
    </dependency>
    <!--xpath 依赖-->
    <dependency>
        <groupId>jaxen</groupId>
        <artifactId>jaxen</artifactId>
        <version>1.2.0</version>
    </dependency>
</dependencies>

# 3.1 读取数据库配置,封装成DataSource对象

  1. 将输入流转换成Document对象,这个Document对象是整个XML文件的内容。
  2. 获取文档的根标签元素。
  3. 使用XPath选择所有的<property>元素,property存放的是数据库名称、账号密码等信息。
  4. 遍历propertyList,将其转换成Properties/HashMap对象
  5. 从Properties获取数据,构造DruidDataSource对象,填充到配置对象中
  6. 获取Mapper文件路径的集合,将SQL语句信息转换成自己创建的mappedStatement对象,存入配置对象的map集合中
    • XMLMapperBuilder对象还没创建,它负责解析Mapper.xml文件,将其转换成mappedStatement集合
点击展开读取配置文件生成DataSources代码
package com.xk857.config;

import com.alibaba.druid.pool.DruidDataSource;
import com.xk857.io.Resources;
import com.xk857.pojo.Configuration;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Element;
import org.dom4j.io.SAXReader;

import java.io.InputStream;
import java.util.List;
import java.util.Properties;

/**
 * @author cv大魔王
 * @version 1.0
 * @description
 * @date 2023/10/4
 */
public class XMLConfigBuilder {

    private final Configuration configuration;

    public XMLConfigBuilder() {
        this.configuration = new Configuration();
    }

    /**
     * 使用dom4j+xpath解析配置文件,封装Configuration对象
     * @param inputStream 输入流对象,即XML文件的内容
     * @return 解析XML填充数据后的Configuration对象
     */
    public Configuration parse(InputStream inputStream) throws DocumentException {
        // 1.将输入流转换成Document对象,Document对象是整个XML文件的内容
        Document document = new SAXReader().read(inputStream);

        // 2.获取文档的根标签元素
        Element rootElement = document.getRootElement();

        // 3.使用XPath选择所有的<property>元素,property存放的是数据库名称、账号密码等信息
        List<Element> propertyList = rootElement.selectNodes("//property");

        // 4.解析list集合,转换成Properties对象(Properties和HashMap类似)
        Properties properties = new Properties();
        for (Element element : propertyList) {
            String name = element.attributeValue("name");
            String value = element.attributeValue("value");
            properties.setProperty(name, value);
        }

        // 5.构造数据源对象,封装到Configuration对象中
        DruidDataSource druidDataSource = new DruidDataSource();
        druidDataSource.setDriverClassName(properties.getProperty("driverClassName"));
        druidDataSource.setUrl(properties.getProperty("url"));
        druidDataSource.setUsername(properties.getProperty("username"));
        druidDataSource.setPassword(properties.getProperty("password"));
        configuration.setDataSource(druidDataSource);

        // 6.获取Mapper文件路径的集合
        List<Element> mapperList = rootElement.selectNodes("//mapper");
        for (Element element : mapperList) {
            String mapperPath = element.attributeValue("resource");
            // 7.根据Mapper文件的路径,获取Mapper文件输入流
            InputStream resourceAsSteam = Resources.getResourceAsSteam(mapperPath);
            // 8.解析Mapper文件的信息,将SQL语句存到传递过去的configuration对象中的mappedStatementMap集合中
            XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(configuration);
            xmlMapperBuilder.parse(resourceAsSteam);
        }
        return configuration;
    }
}

为什么使用Properties而不是HashMap?

PropertiesHashtable的子类,它专门用于存储键值对属性的集合;Properties对象的设计初衷是用于处理属性文件(.properties)中的键值对数据。

虽然Map也可以用于存储属性信息,但它需要手动编写更多的代码来读取和设置属性值,而Properties对象提供了更简洁和方便的方法。

# 3.2 读取Mapper.xml存储的SQL

configuration对象的mappedStatementMap存放所有MappedStatement对象,也就是管理所有SQL的map,key是namespace.方法名

方法接收的输入流是一个XML文件的数据,解析数据封装成MappedStatement对象,添加到configuration的map集合中。

package com.xk857.config;
import com.xk857.pojo.Configuration;
import com.xk857.pojo.MappedStatement;/**
 * @author cv大魔王
 * @version 1.0
 * @description 解析XML文件,生成MappedStatement对象,添加到传递过来的Configuration对象中
 * @date 2023/10/4
 */
public class XMLMapperBuilder {
    
    private final Configuration configuration;
    public XMLMapperBuilder(Configuration configuration) {
        this.configuration = configuration;
    }

    public void parse(InputStream resourceAsSteam) throws DocumentException {
        // 1.将输入流转换成Document对象,存放XML文件的内容
        Document document = new SAXReader().read(resourceAsSteam);
        // 2.获取根节点
        Element rootElement = document.getRootElement();

        /* <select id="selectOne" resultType="com.xk857.pojo.User" parameterType="com.xk857.pojo.User">
                  select * from user where id = #{id} and username = #{username}
           </select> */
        // 3.获取所有select标签,即select查询语句(update/delete等语句后续实现)
        List<Element> selectList = rootElement.selectNodes("//select");
        // 4.获取namespace信息
        String namespace = rootElement.attributeValue("namespace");
        // 5.遍历Select标签,解析标签内容,生成MappedStatement对象,添加到传递过来的Configuration对象中
        for (Element element : selectList) {
            // id一般就是方法名
            String id = element.attributeValue("id");
            String resultType = element.attributeValue("resultType");
            String parameterType = element.attributeValue("parameterType");
            String sql = element.getTextTrim();

            // 5.1 封装mappedStatement对象
            MappedStatement mappedStatement = new MappedStatement();
            String statementId = namespace + "." + id;
            mappedStatement.setStatementId(statementId);
            mappedStatement.setResultType(resultType);
            mappedStatement.setParameterType(parameterType);
            mappedStatement.setSql(sql);
            mappedStatement.setSqlCommandType("select");

            // 5.2将封装好的mappedStatement封装到configuration中的map集合中
            configuration.getMappedStatementMap().put(statementId, mappedStatement);
        }
    }
}

# 3.3 创建SqlSessionFactoryBuilder

package com.xk857.session;
import com.xk857.config.XMLConfigBuilder;
import com.xk857.pojo.Configuration;
import org.dom4j.DocumentException;

public class SqlSessionFactoryBuilder {

    /**
     * 解析配置文件封装成configuration对象,并创建默认工厂
     * @param inputStream xml配置文件内容的输入流
     */
    public SqlSessionFactory build(InputStream inputStream) throws DocumentException {
        // 1.解析配置文件,封装容器对象 XMLConfigBuilder:专门解析核心配置文件的解析类
        XMLConfigBuilder xmlConfigBuilder = new XMLConfigBuilder();
        Configuration configuration =  xmlConfigBuilder.parse(inputStream);
        // 2.创建SqlSessionFactory工厂对象(DefaultSqlSessionFactory下一章创建)
        return new DefaultSqlSessionFactory(configuration);
    }
}
上次更新: 4 个月前