第02篇:Mybatis配置文件解析

西魏陶渊明 ... 2022-3-28 Mybatis 大约 11 分钟

作者: 西魏陶渊明

博客: https://springlearn.cn

真正的猛士,每天干一碗毒鸡汤!

问世间钱为何物,只叫人生死相许。!😄

# 一、配置文件分析

文件分析

在上一篇的代码中,我们看到了一个非常重要文件,这里我们先来人肉分析看,然后看下代码是如何解析的,毕竟代码也是人写的。 思路决定出路,我们如果有思路,然后在看源码会更加的具有分析的能动性。

mybatisConfig.xml



 


















    @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

注意看高亮行

  1. line(4) dtd文件是xml的约束文件,用于约束 xml 标签中属性
  2. line(8) properties标签,指定了配置信息文件是 application.properties
  3. line(11-13) mybatis的配置信息
  4. line(15-27) mybatis支持多环境配置
  5. 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 元素 & 属性 & 属性值

dtd文件 (opens new window)

示例 语法 例子
元素 声明根元素标签 <!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();
    }
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)

重点关注

  1. line(8), 我们看到核心解析类是 XPathParser parser = new XPathParser()
  2. line(17), 标签的解析都在 parseConfiguration
  3. 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

这里就要介绍一个重要的类的,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

  1. 在代码执行的过程中,将关键信息通过 ErrorContext.instance().message() 保存进去。利用到了线程隔离的知识。
  2. ErrorContext.instance() 是利用 ThreadLocal 进行线程隔离。
  3. 异常打印后,进行 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

本文由西魏陶渊明版权所有。如若转载,请注明出处:西魏陶渊明
上次编辑于: 2022年6月16日 21:10
贡献者: lxchinesszz