第02篇:Mybatis配置文件解析
一、配置文件分析
!!! note 文件分析 在上一篇的代码中,我们看到了一个非常重要文件,这里我们先来人肉分析看,然后看下代码是如何解析的,毕竟代码也是人写的。 思路决定出路,我们如果有思路,然后在看源码会更加的具有分析的能动性。 !!!
1 @Test
2 public void mapper() {
3 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
4 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
5 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
6 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
7 // 获取Mybatis配置信息
8 Configuration configuration = sqlSessionFactory.getConfiguration();
9 // 参数: autoCommit,从名字上看就是是否自动提交事务
10 SqlSession sqlSession = sqlSessionFactory.openSession(false);
11 // 获取Mapper
12 TUserMapper mapper = configuration.getMapperRegistry().getMapper(TUserMapper.class, sqlSession);
13 TUser tUser = new TUser();
14 tUser.setName("testUser1");
15 tUser.setTokenId("testTokenId1");
16 mapper.insert(tUser);
17 // 获取插入的数据
18 System.out.println(mapper.selectAll());
19 // 数据插入后,执行查询,然后回滚数据
20 sqlSession.rollback();
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) 映射文件 !!!
基于上面的行,我们来讲解。
1<?xml version="1.0" encoding="UTF-8" ?>
2<!DOCTYPE configuration
3 PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
4 "http://mybatis.org/dtd/mybatis-3-config.dtd">
5<configuration>
6
7 <!-- 指定properties配置文件, 我这里面配置的是数据库相关 -->
8 <properties resource="application.properties"></properties>
9
10 <!-- 指定Mybatis使用log4j -->
11 <settings>
12 <setting name="logImpl" value="LOG4J"/>
13 </settings>
14
15 <environments default="development">
16 <environment id="development">
17 <transactionManager type="JDBC"/>
18 <dataSource type="POOLED">
19 <!-- 上面指定了数据库配置文件, 配置文件里面也是对应的这四个属性 -->
20 <property name="driver" value="${datasource.driver-class-name}"/>
21 <property name="url" value="${datasource.url}"/>
22 <property name="username" value="${datasource.username}"/>
23 <property name="password" value="${datasource.password}"/>
24
25 </dataSource>
26 </environment>
27 </environments>
28
29 <!-- 映射文件,mybatis精髓, 后面才会细讲 -->
30 <mappers>
31 <mapper resource="mapper/TUserMapper.xml"/>
32 </mappers>
33
34</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 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文件,我们知道有那些子标签及属性信息。内容比较长。但是不是很重要。这里只要知道就行。
后面我们看如何使用代码来解析这些标签。
2.3 Mybatis配置解析核心逻辑
!!!tip 思路决定出路
- line(6)
sqlSessionFactory.getConfiguration()
由此来看所有的解析都是在SqlSessionFactoryBuilder进行完成的.
具体的解析xml代码我们不研究,这里我们只要搞清楚它的调用关系,及实现的代码在哪里即可。如果这里
看懂,其实都会得到一个结论。就是mybaits的源码是比较简单的,因为他的配置是比较集中的,无论是xml方式或者是注解方式。
最终所有的配置信息都在 Configuration 类中。
!!!
1 @Test
2 public void configuration() {
3 // 读取配置信息(为什么路径前不用加/,因为是相对路径。maven编译后的资源文件和class文件都是在一个包下,所以不用加/就是当前包目录)
4 InputStream mapperInputStream = Thread.currentThread().getContextClassLoader().getResourceAsStream("mybatisConfig.xml");
5 // 生成SqlSession工厂,SqlSession从名字上看就是,跟数据库交互的会话信息,负责将sql提交到数据库进行执行
6 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(mapperInputStream, "development");
7 // 获取Mybatis配置信息,由此来看所有的解析都是在SqlSessionFactoryBuilder进行完成的.
8 Configuration configuration = sqlSessionFactory.getConfiguration();
9 }2.3.1 new SqlSessionFactoryBuilder().build
这里可以看到就是核心类就是使用 XMLConfigBuilder 进行解析。下面我们就主要分析 XMLConfigBuilder
1public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
2 try {
3 XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
4 return build(parser.parse());
5 } catch (Exception e) {
6 throw ExceptionFactory.wrapException("Error building SqlSession.", e);
7 } finally {
8 ErrorContext.instance().reset();
9 try {
10 inputStream.close();
11 } catch (IOException e) {
12 // Intentionally ignore. Prefer previous error.
13 }
14 }
15 }2.3.2 核心配置类解析(XMLConfigBuilder)
!!!note 重点关注
- line(8), 我们看到核心解析类是
XPathParser parser = new XPathParser() - line(17), 标签的解析都在
parseConfiguration - line(17), 思考下为什么先解析
propertiesElement(root.evalNode("properties"))!!!
1public class XMLConfigBuilder extends BaseBuilder {
2
3 private boolean parsed;
4 private final XPathParser parser;
5 private String environment;
6 private final ReflectorFactory localReflectorFactory = new DefaultReflectorFactory();
7
8 public Configuration parse() {
9 if (parsed) {
10 throw new BuilderException("Each XMLConfigBuilder can only be used once.");
11 }
12 parsed = true;
13 parseConfiguration(parser.evalNode("/configuration"));
14 return configuration;
15 }
16
17 private void parseConfiguration(XNode root) {
18 try {
19 // issue #117 read properties first
20 propertiesElement(root.evalNode("properties"));
21 Properties settings = settingsAsProperties(root.evalNode("settings"));
22 loadCustomVfs(settings);
23 loadCustomLogImpl(settings);
24 typeAliasesElement(root.evalNode("typeAliases"));
25 pluginElement(root.evalNode("plugins"));
26 objectFactoryElement(root.evalNode("objectFactory"));
27 objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
28 reflectorFactoryElement(root.evalNode("reflectorFactory"));
29 settingsElement(settings);
30 // read it after objectFactory and objectWrapperFactory issue #631
31 environmentsElement(root.evalNode("environments"));
32 databaseIdProviderElement(root.evalNode("databaseIdProvider"));
33 typeHandlerElement(root.evalNode("typeHandlers"));
34 mapperElement(root.evalNode("mappers"));
35 } catch (Exception e) {
36 throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
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#
1 <!-- 映射文件,mybatis精髓, 后面才会细讲 -->
2 <mappers>
3 <mapper resource="mapper/TUserMapper.xml"/>
4 </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在解析的时候会自动将 ${} 填充上真实的数据。
1 // 执行后,会解析properties标签,并且将属性赋值给XPathParser
2 propertiesElement(root.evalNode("properties"));
3 parser.setVariables(defaults);
4 configuration.setVariables(defaults);
5
6 // XPathParser 生成节点时候,属性信息会提前处理。
7 public XNode(XPathParser xpathParser, Node node, Properties variables) {
8 this.xpathParser = xpathParser;
9 this.node = node;
10 this.name = node.getNodeName();
11 this.variables = variables;
12 this.attributes = parseAttributes(node);
13 this.body = parseBody(node);
14 }
15 // 发现是占位符,就从变量中读取。
16 // ${datasource.driver-class-name} 替换成变量值里面的数据。
17 public static String parse(String string, Properties variables) {
18 VariableTokenHandler handler = new VariableTokenHandler(variables);
19 GenericTokenParser parser = new GenericTokenParser("${", "}", handler);
20 return parser.parse(string);
21 }2.4.2 Mybatis Resources 工具
可以从配置文件中或者网络中解析配置,生成 Resources 对象
1 String resource = context.getStringAttribute("resource");
2 if (resource != null) {
3 defaults.putAll(Resources.getResourceAsProperties(resource));
4 } else if (url != null) {
5 defaults.putAll(Resources.getUrlAsProperties(url));
6 }
7 parser.setVariables(defaults);
8 configuration.setVariables(defaults);
9
10 // 从资源中获取流
11 InputStream inputStream = Resources.getResourceAsStream(resource)
12 // 从url中获取流
13 InputStream inputStream = Resources.getUrlAsStream(url)2.4.3 Mybatis PropertyParser 占位符解析
1 @Test
2 public void propertyParser() {
3 Properties variables = new Properties();
4 variables.put("datasource.driver-class-name", "com.mysql.cj.jdbc.Driver");
5 // 变量中有就从变量中获取 参数信息: com.mysql.cj.jdbc.Driver
6 System.out.println(PropertyParser.parse("参数信息: ${datasource.driver-class-name}", variables));
7 // 变量中没有就直接返回key datasource.url
8 System.out.println(PropertyParser.parse("datasource.url", variables));
9 }2.4.4 反射工厂 ReflectorFactory
在Mybatis中使用到的反射地方蛮多的,那么都知道反射是相对比较耗时间,那么我们来看Mybatis是如何利用反射工厂来提高反射的性能的?
缓存,对要使用的Class类,做反射并保存起来, 生成的对象是 Reflector。
ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
1public interface ReflectorFactory {
2
3 boolean isClassCacheEnabled();
4
5 void setClassCacheEnabled(boolean classCacheEnabled);
6
7 Reflector findForClass(Class<?> type);
8}
9
10public class Reflector {
11
12 private final Class<?> type;
13 private final String[] readablePropertyNames;
14 private final String[] writablePropertyNames;
15 private final Map<String, Invoker> setMethods = new HashMap<>();
16 private final Map<String, Invoker> getMethods = new HashMap<>();
17 private final Map<String, Class<?>> setTypes = new HashMap<>();
18 private final Map<String, Class<?>> getTypes = new HashMap<>();
19 private Constructor<?> defaultConstructor;
20
21 private Map<String, String> caseInsensitivePropertyMap = new HashMap<>();
22}
1 @Test
2 public void reflector() throws Exception {
3 ReflectorFactory reflectorFactory = new DefaultReflectorFactory();
4 Reflector forClass = reflectorFactory.findForClass(TUser.class);
5 TUser user = (TUser) forClass.getDefaultConstructor().newInstance();
6 forClass.getSetInvoker("uid").invoke(user, new Object[]{1});
7 forClass.getSetInvoker("name").invoke(user, new Object[]{"孙悟空"});
8 forClass.getSetInvoker("tokenId").invoke(user, new Object[]{"tokenId"});
9 // 1
10 System.out.println(forClass.getGetInvoker("uid").invoke(user, new Object[]{}));
11 // 孙悟空
12 System.out.println(forClass.getGetInvoker("name").invoke(user, new Object[]{}));
13 }2.4.5 异常上下文设计 ErrorContext
- 在代码执行的过程中,将关键信息通过
ErrorContext.instance().message()保存进去。利用到了线程隔离的知识。 ErrorContext.instance()是利用ThreadLocal进行线程隔离。- 异常打印后,进行
reset重置。
1 public int update(String statement, Object parameter) {
2 try {
3 dirty = true;
4 MappedStatement ms = configuration.getMappedStatement(statement);
5 return executor.update(ms, wrapCollection(parameter));
6 } catch (Exception e) {
7 throw wrapException("Error updating database. Cause: " + e, e);
8 } finally {
9 // 完成之后异常上下文进行重置
10 ErrorContext.instance().reset();
11 }
12 }
13
14 // 将异常上线文中报错的错误都打印出来。
15 public static RuntimeException wrapException(String message, Exception e) {
16 return new PersistenceException(ErrorContext.instance().message(message).cause(e).toString(), e);
17 }