第02篇:Mybatis配置文件解析
作者: 西魏陶渊明
博客: https://blog.springlearn.cn/
西魏陶渊明
莫笑少年江湖梦,谁不少年梦江湖
一、配置文件分析
文件分析
在上一篇的代码中,我们看到了一个非常重要文件,这里我们先来人肉分析看,然后看下代码是如何解析的,毕竟代码也是人写的。
思路决定出路,我们如果有思路,然后在看源码会更加的具有分析的能动性。
@Test
public void mapper() {
// 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
// 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
// 获取Mybatis配置信息
Configuration configuration = sqlSessionFactory.getConfiguration();
// 参数: autoCommit,从名字上看就是是否自动提交事务
SqlSession sqlSession = sqlSessionFactory.openSession(false);
// 获取Mapper
TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
TUser tUser = new TUser();
tUser.setName("testUser1");
tUser.setTokenId("testTokenId1");
mapper.insert(tUser);
// 获取插入的数据
System.out.println(mapper.selectAll());
// 数据插入后,执行查询,然后回滚数据
sqlSession.rollback();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
1.1 mybatisConfig.xml
::: tip 注意看高亮行
- line(4) dtd文件是xml的约束文件,用于约束
xml
标签中属性 - line(8) properties标签,指定了配置信息文件是
application.properties
- line(11-13) mybatis的配置信息
- line(15-27) mybatis支持多环境配置
- line(30-32) 映射文件
:::
基于上面的行,我们来讲解。
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 指定properties配置文件, 我这里面配置的是数据库相关 -->
<properties resource="application.properties"></properties>
<!-- 指定Mybatis使用log4j -->
<settings>
<setting name="logImpl" value="LOG4J"/>
</settings>
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->
<property name="driver" value="${datasource.driver-class-name}"/>
<property name="url" value="${datasource.url}"/>
<property name="username" value="${datasource.username}"/>
<property name="password" value="${datasource.password}"/>
</dataSource>
</environment>
</environments>
<!-- 映射文件,mybatis精髓, 后面才会细讲 -->
<mappers>
<mapper resource="mapper/TUserMapper.xml"/>
</mappers>
</configuration>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
二、知识点讲解
2.1 xml约束文件dtd
为什么要学习dtd约束文件呢? 当你学会dtd约束文件后,你就知道这个标签有那些属性,知道标签及子标签信息。
当有一天你要写开源框架的时候,你也可以来定义你自己的配置文件规则。这部分知识了解就行。不需要死记硬背。
因为记住也基本没啥用,只要做到看到了认识,需要用了知道去哪里抄代码学习就够了。
2.1.1 元素 & 属性 & 属性值
域 | 示例 | 语法 | 例子 |
---|---|---|---|
元素 | 声明根元素标签 | <!ELEMENT 元素名称 (元素内容)> |
<!ELEMENT students(student)> ,元素students有一个student |
元素 | 空元素 | <!ELEMENT 元素名称 EMPTY> |
<br /> |
元素 | 元素只出现一次 | <!ELEMENT 元素名称 (子元素名称)> |
<!ELEMENT students(student)> ,元素students至少有一个student |
元素 | 元素最少出现一次 | <!ELEMENT 元素名称 (子元素名称+)> |
<!ELEMENT students(student+)> ,元素students最少有一个student |
元素 | 声明出现零次或多次的元素 | <!ELEMENT 元素名称 (子元素名称*)> |
<!ELEMENT students(student*)> ,元素students可以有多个student,也可以一个没有 |
元素 | 声明“非…/既…”类型的内容 | `<!ELEMENT note (to,from,header,(message | body))>` |
元素 | 声明混合型的内容 | `<!ELEMENT note (#PCDATA | to |
属性 | 属性声明 | <!ATTLIST 元素名称 属性名称 属性类型 默认值> |
<!ATTLIST payment type CDATA "check"> ,payment有一个属性type,类型为字符类型,默认值check |
<!ATTLIST 元素名称 属性名称 属性类型 默认值>
值类型
类型 | 描述 |
---|---|
CDATA | 值为字符数据 (character data) |
(en1 | en2 |
ID | 值为唯一的 id |
IDREF | 值为另外一个元素的 id |
IDREFS | 值为其他 id 的列表 |
NMTOKEN | 值为合法的 XML 名称 |
NMTOKENS | 值是一个实体 |
ENTITIES | 值是一个实体列表 |
NOTATION | 此值是符号的名称 |
xml: | 值是一个预定义的 XML 值 |
默认值参数可使用下列值
类型 | 描述 |
---|---|
值 | 属性的默认值 |
#REQUIRED | 属性值是必需的 |
#IMPLIED | 属性不是必需的 |
#FIXED value | 属性值是固定的 |
2.2 configuration标签分析
前面我们知道了dtd约束文件,我们就可以看下,configuration标签一共有那些子标签及属性信息了。
通过分析dtd文件,我们知道有那些子标签及属性信息。内容比较长。但是不是很重要。这里只要知道就行。
后面我们看如何使用代码来解析这些标签。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QUapYFs-1658221608296)(/blog/img/Mybatis配置标签.svg)]
2.3 Mybatis配置解析核心逻辑
:::tip 思路决定出路
- line(6)
sqlSessionFactory.getConfiguration()
由此来看所有的解析都是在SqlSessionFactoryBuilder进行完成的.
具体的解析xml代码我们不研究,这里我们只要搞清楚它的调用关系,及实现的代码在哪里即可。如果这里
看懂,其实都会得到一个结论。就是mybaits的源码是比较简单的,因为他的配置是比较集中的,无论是xml方式或者是注解方式。
最终所有的配置信息都在 Configuration
类中。
:::
@Test
public void configuration() {
// 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
// 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
// 获取Mybatis配置信息,由此来看所有的解析都是在SqlSessionFactoryBuilder进行完成的.
Configuration configuration = sqlSessionFactory.getConfiguration();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
2.3.1 new SqlSessionFactoryBuilder().build
这里可以看到就是核心类就是使用 XMLConfigBuilder
进行解析。下面我们就主要分析 XMLConfigBuilder
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
inputStream.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
2.3.2 核心配置类解析(XMLConfigBuilder)
:::note 重点关注
- line(8), 我们看到核心解析类是
XPathParser parser = new XPathParser()
- line(17), 标签的解析都在
parseConfiguration
- line(17), 思考下为什么先解析
propertiesElement(root.evalNode("properties"))
:::
public class XMLConfigBuilder extends BaseBuilder {
private boolean parsed;
private final XPathParser parser;
private String environment;
private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
public Configuration parse() {
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
private void parseConfiguration(XNode root) {
try {
// issue #117 read properties first
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
typeHandlerElement(root.evalNode("typeHandlers"));
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
看到上面代码是不是就恍然大悟了,原来配置文件的标签都是在这里解析呀。这里的主要思路就是将xml解析成Java对象然后放到
Configuration中。具体任何实现呢? 感兴趣可以自己研究下。
2.3.3 Configuration属性介绍
那么这些数据最终哪里会使用呢,我们专门留一片文章, 详细分析。这里先看看Configuration内部都有那些关键的配置类把。
属性 | 解释 |
---|---|
TypeAliasRegistry | key是一个别名,value是一个class对象 |
Properties variables | 配置文件中占位符的变量配置 |
InterceptorChain interceptorChain | 拦截链,用于拦截方法,实现插件 |
ObjectFactory objectFactory | 对象实例化统一的工厂方法,我们不用都反射来实例化了 |
ObjectWrapperFactory objectWrapperFactory | 包装对象后为其提供统一的属性操作方法。我们不用通过反射来修改对象属性值了 |
ReflectorFactory reflectorFactory | 反射工厂,用于生成一个反射信息对象,具有缓存的作用 |
Environment environment | 环境信息包含(事务管理器和数据源) |
TypeHandlerRegistry typeHandlerRegistry | 主要处理jdbc的返回数据,转换成Java对象 |
MapperRegistry mapperRegistry | Mapper生成的处理类,包含代理的逻辑 |
2.3.4 Mapper.xml 解析
XMLMapperBuilder
解析Mapper对应的xml配置文件,这里面包含了sql的信息。
mapper的dtd约束文件更多,可以参考: https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#
<!-- 映射文件,mybatis精髓, 后面才会细讲 -->
<mappers>
<mapper resource="mapper/TUserMapper.xml"/>
</mappers>
- 1
- 2
- 3
- 4
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-u7T4vydz-1658221608297)(/blog/img/Mapper.svg)]
这里就要介绍一个重要的类的,MapperBuilderAssistant
Mapper构建辅助工具类。
属性 | 解释 |
---|---|
MapperBuilderAssistant | Mapper构建辅助工具类(缓存配置) |
CacheRefResolver | 决定如何使用缓存 |
ParameterMapping | 当sql中使用到了#{}占位符时候,负责填充sql参数 |
ResultMapResolver | 返回值映射 |
Map<String, XNode> sqlFragments | sql片段 |
MappedStatement | Mapper方法的所有信息(出参,入参,及sql信息等) |
2.4 Mybatis可以借鉴的知识点
2.4.1 占位符解析逻辑
在第一篇的时候我们说过,从配置文件解析中我们能学会,如果解析占位符。并将占位符填充真实数据。这里我们就具体说下是如何解析。
还记得前面让思考下为什么先解析 propertiesElement(root.evalNode("properties"))
。
答案就是为了先读取变量信息,方便后面给依赖的信息,给填充值。
我们直接说答案: 具体谁来做了这个事情,从职责划分上来看,这个其实还是属于xml文件解析。所以是 XPathParser parser
XPathParser中填充上变量信息,这样XPathParser在解析的时候会自动将 ${}
填充上真实的数据。
// 执行后,会解析properties标签,并且将属性赋值给XPathParser
propertiesElement(root.evalNode("properties"));
parser.setVariables(defaults);
configuration.setVariables(defaults);
// XPathParser 生成节点时候,属性信息会提前处理。
public XNode(XPathParser xpathParser, Node node, Properties variables) {
this.xpathParser = xpathParser;
this.node = node;
this.name = node.getNodeName();
this.variables = variables;
this.attributes = parseAttributes(node);
this.body = parseBody(node);
}
// 发现是占位符,就从变量中读取。
// ${datasource.driver-class-name} 替换成变量值里面的数据。
public static String parse(String string, Properties variables) {
VariableTokenHandler handler = new VariableTokenHandler(variables);
GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
return parser.parse(string);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
2.4.2 Mybatis Resources 工具
可以从配置文件中或者网络中解析配置,生成 Resources
对象
String resource = context.getStringAttribute("resource");
if (resource != null) {
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
defaults.putAll(Resources.getUrlAsProperties(url));
}
parser.setVariables(defaults);
configuration.setVariables(defaults);
// 从资源中获取流
InputStream inputStream = Resources.getResourceAsStream(resource)
// 从url中获取流
InputStream inputStream = Resources.getUrlAsStream(url)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
2.4.3 Mybatis PropertyParser 占位符解析
@Test
public void propertyParser() {
Properties variables = new Properties();
variables.put("datasource.driver-class-name", "com.mysql.cj.jdbc.Driver");
// 变量中有就从变量中获取 参数信息: com.mysql.cj.jdbc.Driver
System.out.println(PropertyParser.parse("参数信息: ${datasource.driver-class-name}", variables));
// 变量中没有就直接返回key datasource.url
System.out.println(PropertyParser.parse("datasource.url", variables));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
2.4.4 反射工厂 ReflectorFactory
在Mybatis中使用到的反射地方蛮多的,那么都知道反射是相对比较耗时间,那么我们来看Mybatis是如何利用反射工厂来提高反射的性能的?
缓存,对要使用的Class类,做反射并保存起来, 生成的对象是 Reflector
。
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
public interface ReflectorFactory {
boolean isClassCacheEnabled();
void setClassCacheEnabled(boolean classCacheEnabled);
Reflector findForClass(Class<?> type);
}
public class Reflector {
private final Class<?> type;
private final String[] readablePropertyNames;
private final String[] writablePropertyNames;
private final Map<String, Invoker> setMethods = new HashMap<>();
private final Map<String, Invoker> getMethods = new HashMap<>();
private final Map<String, Class<?>> setTypes = new HashMap<>();
private final Map<String, Class<?>> getTypes = new HashMap<>();
private Constructor<?> defaultConstructor;
private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
@Test
public void reflector() throws Exception {
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
Reflector forClass = reflectorFactory.findForClass(TUser.class);
TUser user = (TUser) forClass.getDefaultConstructor().newInstance();
forClass.getSetInvoker("uid").invoke(user, new Object[]{1});
forClass.getSetInvoker("name").invoke(user, new Object[]{"孙悟空"});
forClass.getSetInvoker("tokenId").invoke(user, new Object[]{"tokenId"});
// 1
System.out.println(forClass.getGetInvoker("uid").invoke(user, new Object[]{}));
// 孙悟空
System.out.println(forClass.getGetInvoker("name").invoke(user, new Object[]{}));
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
2.4.5 异常上下文设计 ErrorContext
- 在代码执行的过程中,将关键信息通过
ErrorContext.instance().message()
保存进去。利用到了线程隔离的知识。 ErrorContext.instance()
是利用ThreadLocal
进行线程隔离。- 异常打印后,进行
reset
重置。
public int update(String statement, Object parameter) {
try {
dirty = true;
MappedStatement ms = configuration.getMappedStatement(statement);
return executor.update(ms, wrapCollection(parameter));
} catch (Exception e) {
throw wrapException("Error updating database. Cause: " + e, e);
} finally {
// 完成之后异常上下文进行重置
ErrorContext.instance().reset();
}
}
// 将异常上线文中报错的错误都打印出来。
public static RuntimeException wrapException(String message, Exception e) {
return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。
原文链接:springlearn.blog.csdn.net/article/details/125876265
- 点赞
- 收藏
- 关注作者
评论(0)