手写mybatis解析流程
上一章我们分析了mybatis的架构和流程,并将整个流程分成了两部分
- 解析流程
- 执行流程
本章将在上一章的基础上,开始手写mybatis框架的解析流程。
流程分析
解析流程简单说就是将核心配置信息,以及映射配置信息,从xml形式或者注解等形式解析成java对象形式保存到内存中,为整个框架运行提供支撑,但是并不是简单的保存数据,因为有些数据还需要通过一些处理才能够使用,例如映射文件的sql信息,从xml中拿来的并不能直接被JDBC所执行,还需要一些处理,所以我所说的解析,除了保存数据之外,更重要的一步是提供数据的使用方式。
而解析流程最关键最难的地方在于sql的解析,如何将配置文件里面的sql语句最终解析成JDBC可以执行的语句,需要通过两个过程实现,一个是拼接过程,一个是解析过程,上一章已经分析过了,所以整个解析流程我们按照下面三部分依次讲解:
- 核心配置文件解析
- 映射配置文件解析
- sql解析(拼接过程和解析过程)
1. 核心配置文件解析
先把我们的核心配置文件mybatis-config-schema.xml展示出来:
<?xml version="1.0" encoding="UTF-8" ?>
<configuration xmlns="http://www.aiduoduo.site/schema/mybatis" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.aiduoduo.site/schema/mybatis site/aiduoduo/mybatis/builder/xml/xsd/mybatis-config.xsd">
<properties resource="datasource.properties"/>
<enviroments default="dev">
<enviroment id="dev">
<datasource type="druid">
<property name="driverClassName" value="${db.driver}"/>
<property name="url" value="${db.url}"/>
<property name="username" value="${db.username}"/>
<property name="password" value="${db.password}"/>
</datasource>
</enviroment>
</enviroments>
<mappers>
<mapper resource="mybatis-mapper.xml"/>
</mappers>
</configuration>
这个核心配置文件,我是用的schema约束,想要了解schema语法的,请走传送门,不会也不影响,和dtd一样,就是用来约束xml内容的。
通过这个配置文件,可以看到,我会实现以下几个小功能:
- 外部加载properties配置
- 使用连接池获取连接,我们只支持了一种druid的连接池
- 触发映射文件的解析
首先解析xml文件,最终解析出的信息需要有一个核心对象进行保存:
我们用Configuration对象存储核心配置文件里面的信息
/**
* @Author yangtianhao
* @Date 2020/2/5 5:28 下午
* @Version 1.0
*/
public class Configuration {
private Environment environment;
private Properties properties;
private Map<String, MappedStatment> mappedStatmentMap = new HashMap<>();
public Environment getEnvironment() {
return environment;
}
public void setEnvironment(Environment environment) {
this.environment = environment;
}
public void addMappedStatment(MappedStatment mappedStatment) {
mappedStatmentMap.put(mappedStatment.getId(), mappedStatment);
}
public Properties getProperties() {
return properties;
}
public void setProperties(Properties properties) {
this.properties = properties;
}
public MappedStatment getMappedStatment(String id) {
return mappedStatmentMap.get(id);
}
}
它有三个成员变量:
Enviroment:保存环境相关信息,主要维护了连接池
properties:保存外部加载的properties文件内容
Map<String, MappedStatment>:保存映射文件信息解析后的statment信息
其中Enviroment对象,维护了连接池以及id属性。
package site.aiduoduo.mybatis.mapping;
import javax.sql.DataSource;
/**
* @Author yangtianhao
* @Date 2020/2/5 5:29 下午
* @Version 1.0
*/
public class Environment {
private String id;
private DataSource dataSource;
public Environment(String id, DataSource dataSource) {
this.id = id;
this.dataSource = dataSource;
}
public String getId() {
return id;
}
public DataSource getDataSource() {
return dataSource;
}
}
然后我们使用建造者设计模式,使用XmlConfigurationBuilder对象负责Configuration的构建,它会对xml进行解析,将Configuration的构建过程分成多个子步骤,依次执行完成后最终返回给我们Configuration对象
/**
* @Author yangtianhao
* @Date 2020/2/7 8:39 下午
* @Version 1.0
*/
public class XmlConfigurationBuilder {
private Configuration configuration;
private XpathParser xpathParser;
public XmlConfigurationBuilder(InputStream inputStream) {
xpathParser = new XpathParser(inputStream);
configuration = new Configuration();
}
public Configuration parse() throws Exception {
parseProperties(xpathParser.selectSingleElement("src:configuration/src:properties"));
parseEnviroments(xpathParser.selectSingleElement("src:configuration/src:enviroments"));
parseMappers(xpathParser.selectSingleElement("src:configuration/src:mappers"));
return configuration;
}
private void parseMappers(Element mappersElement) {
List<Element> mapperList = mappersElement.elements("mapper");
if(CollectionUtils.isNotEmpty(mapperList)){
for (Element element : mapperList) {
String resource = element.attributeValue("resource");
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(resource);
XmlMapperBuilder xmlMapperBuilder = new XmlMapperBuilder(configuration, resourceAsStream);
xmlMapperBuilder.parse();
}
}
}
private void parseEnviroments(Element enviromentsElement) throws Exception {
String defaultEnviromentId = enviromentsElement.attributeValue("default");
List<Element> enviromentList = enviromentsElement.elements("enviroment");
for (Element element : enviromentList) {
String id = element.attributeValue("id");
if (StringUtils.equals(defaultEnviromentId, id)) {
Element datasource = element.element("datasource");
String type = datasource.attributeValue("type");
if ("druid".equals(type)) {
GenericTokenParser genericTokenParser = new GenericTokenParser("${", "}", new PropertyHandler());
Map<String, String> dataSourceProperty = new HashMap();
List<Element> propertyList = datasource.elements("property");
for (Element property : propertyList) {
String name = property.attributeValue("name");
String value = property.attributeValue("value");
String parse = genericTokenParser.parse(value);
dataSourceProperty.put(name, configuration.getProperties().getProperty(parse));
}
DataSource dataSource = DruidDataSourceFactory.createDataSource(dataSourceProperty);
configuration.setEnvironment(new Environment(defaultEnviromentId, dataSource));
}
break;
}
}
}
private void parseProperties(Element propertiesElement) {
String resource = propertiesElement.attributeValue("resource");
if (StringUtils.isNotBlank(resource)) {
InputStream resourceAsStream = this.getClass().getClassLoader().getResourceAsStream(resource);
Properties properties = new Properties();
try {
properties.load(resourceAsStream);
} catch (IOException e) {
e.printStackTrace();
}
configuration.setProperties(properties);
}
}
}
需要关注的点:
-
成员属性:XpathParser对象
可以看到该对象是在XmlConfigurationBuilder构造函数中创建的,并且它的构造接收的是InputStream对象,该对象就是对应的xml配置文件的输入流,其实XpathParser类的职责就是专门用来解析xml文件,获取xml元素的,它的底层封装的是dom4j,维护了一个Document对象,创建该类意义在于:我们将所有的对xml的操作都委托给该类,具有很好的复用性,和隔离性。
需要注意的是,上面例子中在使用selectSingleElement方法的时候,底层用的是xpath获取元素,但是使用xpath语法的时候,我是这种格式src:configuration/src:properties,有src这个前缀,这是因为我们用的是schema约束,它的xml中有名称空间导致的。 -
XmlConfigurationBuilder构造函数
(1)构造中传InputStream,该流就是需要加载的核心配置文件的输入流,解析肯定要需要先知道解析哪个文件,然后委托给XpathParser进行解析。
(2)实例化一个Configuration对象,该对象内部属性都是空的,等解析的时候会对该对象进行初始化,一步步的封装数据,等解析完成,最终会将该对象返回。 -
parse()方法
(1)真正的解析方法,当调用该方法开始解析,将整个Configuration对象的构建分成了多个步骤,等解析完所有步骤后,最终会将Configuration对象返回
(2)解析分三个步骤(当然真正的mybatis远不只三个步骤。。),解析外部properties,解析enviroment环境,解析mapper映射信息
3.1 parseProperties()方法
比较简单,获取properties标签的resource属性,加载properties文件并保存到Configuration对象中
3.2 parseEnviroments()方法
加载enviroments标签,根据default值获取默认指定的环境配置信息,通过解析property标签的value属性,拿到key ,再从properties中获取真正的值,进行连接池的构建,并保存到Configuration对象中。
其中涉及到GenericTokenParser和PropertyHandler两个类,作用就是将例如"{db.url}" 的字符串,解析成"url",GenericTokenParser这个类后面解析sql的时候也会用,它的构造函数有三个参数public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {...},主要功能就是用来解析字符串,将字符串中被openToken开头,closeToken结尾包含的字符串替换成其他字符串,替换成什么样子就是通过传自己实现的TokenHandler决定的。
3.3 parseMappers()方法
解析mappers标签,获取所有子mapper标签的resource属性,通过属性获取对应的文件流,再委托给XmlMapperBuilder进行解析,可以看到XmlMapperBuilder进行解析,需要知道核心对象configuration才能对其进行mapper的初始化。XmlMapperBuilder这个类的职责就是专门负责解析映射配置文件的。
3.2中提到的GenericTokenParser和PropertyHandler附上源码,看一下,后面再用到的时候就不说了:
/*
* 这个类我是直接从源码拷贝的,本身没什么复杂的就是专门解析字符串替换字符串中被开始记号和结束记号包含的内容
*/
public class GenericTokenParser {
//有一个开始和结束记号
private final String openToken;
private final String closeToken;
//记号处理器,需要我们自己实现一个处理方式
private final TokenHandler handler;
//构造中需要传入开始标记、结束标记,以及记号处理器
public GenericTokenParser(String openToken, String closeToken, TokenHandler handler) {
this.openToken = openToken;
this.closeToken = closeToken;

本文详细解析了手写MyBatis框架的解析流程,包括核心配置文件、映射配置文件的解析以及SQL语句的解析过程。通过构建Configuration对象,解析XML配置,处理动态SQL,最终生成可执行SQL。
最低0.47元/天 解锁文章
1556

被折叠的 条评论
为什么被折叠?



