Appearance
流程步骤分析
如下图所示,我们配合图片分析如何搭建持久层模块,该如何开发?
- 读取配置文件:创建Resources类,负责加载配置文件,将配置文件加载成字节输入流,存入内存方便调用。
- 方法:InputStream getResourcesAsStream(String path) {}
- 传递的path路径就是sqlMapConfig.xml文件的路径,这个文件存储数据库的配置信息;而Mapper.xml存放SQL语句,这个暂时不管它。
- 创建两个JavaBean。
- Configuration:全局配置类,存储解析出来的SqlMapConfig.xml文件的内容
- MappedStatement:映射配置类,存储解析mapper,xml文件的内容
- 解析配置文件,填充容器对象;
- 创建SqlSessionFactoryBuilder类,方法SqlSessionFactory build(InputStream in) {}
- 这个方法干两件事,①解析配置文件到Configuration;②创建DeafultSqlSessionFactory
- 创建SqlSession接口及实现类DefaultSqlSessionFactory
- 方法:SqlSession openSession();
- 虽然说本质上就是为了拿到SqlSession对象,创建Factory类看着很多余,但是工厂设计模式可以让你传递不同参数,拿到的SqlSession是不同的,后续代码会有详细提现。
- 创建SqlSession接口及实现类DefaultSqlSession,方法:selectList、SelectOne
- 创建Executor接口及实现类SimpleExecutor,执行底层的JDBC
创建配置文件
resources目录下的结构如下所示
md
|-- resources
|-- sqlMapConfig.xml
|-- mapper
|-- ProductMapper.xml
|-- UserMapper.xml
想一想配置文件在哪创建?是不是在使用端创建,我们不可能在编写框架的时候就强制定义你的数据库信息,因此肯定是在使用方创建sqlMapConfig.xml
文件
xml
<configuration>
<!--1.配置数据库信息-->
<dataSource>
<property name="driverClassName" value="com.mysql.jdbc.Driver"></property>
<property name="url" value="jdbc:mysql:///test_mybatis?useSSL=false&characterEncoding=UTF-8&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如下所示:
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目录下的资源文件,资源路径通过参数传递,详细解释如下:
- 调用
Resources.class.getClassLoader()
返回了与Resources
类关联的类加载器。 getResourceAsStream()
用于获取resources
目录下指定路径的文件,返回输入流对象用于读取资源内容。
java
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);
}
}
}
在使用端编写测试类。
java
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语句、返回值类型、参数类型等
java
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语句
java
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文件,需要用到如下两个依赖,没用过没关系,注释写的很详细。
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对象
- 将输入流转换成Document对象,这个Document对象是整个XML文件的内容。
- 获取文档的根标签元素。
- 使用XPath选择所有的
<property>
元素,property存放的是数据库名称、账号密码等信息。 - 遍历propertyList,将其转换成Properties/HashMap对象
- 从Properties获取数据,构造DruidDataSource对象,填充到配置对象中
- 获取Mapper文件路径的集合,将SQL语句信息转换成自己创建的mappedStatement对象,存入配置对象的map集合中
- XMLMapperBuilder对象还没创建,它负责解析Mapper.xml文件,将其转换成mappedStatement集合
点击展开读取配置文件生成DataSources代码
java
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?
Properties
是Hashtable
的子类,它专门用于存储键值对属性的集合;Properties
对象的设计初衷是用于处理属性文件(.properties)中的键值对数据。
虽然Map
也可以用于存储属性信息,但它需要手动编写更多的代码来读取和设置属性值,而Properties
对象提供了更简洁和方便的方法。
3.2 读取Mapper.xml存储的SQL
configuration
对象的mappedStatementMap
存放所有MappedStatement
对象,也就是管理所有SQL的map,key是namespace.方法名
。
方法接收的输入流是一个XML文件的数据,解析数据封装成MappedStatement对象,添加到configuration
的map集合中。
java
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
java
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);
}
}