第02篇:Mybatis配置文件解析
作者: 西魏陶渊明
博客: https://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.1 mybatisConfig.xml
注意看高亮行
- 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>
# 二、知识点讲解
# 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 student(name,age,(boy|girl))> ,元素student有一个name和age标签,有一个boy或者girl标签 |
元素 | 声明混合型的内容 | <!ELEMENT note (#PCDATA|to|from|header|message)*> |
<!ELEMENT note (#PCDATA|to|from|header|message)*> "note" 元素可包含出现零次或多次的 PCDATA、"to"、"from"、"header" 或者 "message" |
属性 | 属性声明 | <!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标签一共有那些子标签及属性信息了。
mybatis-3-config.dtd (opens new window)
通过分析dtd文件,我们知道有那些子标签及属性信息。内容比较长。但是不是很重要。这里只要知道就行。
后面我们看如何使用代码来解析这些标签。
# 2.3 Mybatis配置解析核心逻辑
思路决定出路
- 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();
-
}
# 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.
-
}
-
}
-
}
# 2.3.2 核心配置类解析(XMLConfigBuilder)
重点关注
- 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);
-
}
-
}
-
}
看到上面代码是不是就恍然大悟了,原来配置文件的标签都是在这里解析呀。这里的主要思路就是将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>
这里就要介绍一个重要的类的,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);
-
}
# 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)
# 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));
-
}
# 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<>();
-
}
-
@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[]{}));
-
}
# 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);
-
}
文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。
原文链接:springlearn.blog.csdn.net/article/details/125858037
- 点赞
- 收藏
- 关注作者
评论(0)