第02篇:Mybatis配置文件解析

举报
西魏陶渊明 发表于 2022/09/25 02:58:36 2022/09/25
【摘要】 作者: 西魏陶渊明 博客: https://blog.springlearn.cn/ 西魏陶渊明 莫笑少年江湖梦,谁不少年梦江湖 一、配置文件分析 文件分析 在上一篇的代码中,我...

作者: 西魏陶渊明
博客: 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 注意看高亮行

  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文件

示例 语法 例子
元素 声明根元素标签 <!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标签一共有那些子标签及属性信息了。

mybatis-3-config.dtd

通过分析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 重点关注

  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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(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

  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

文章来源: springlearn.blog.csdn.net,作者:西魏陶渊明,版权归原作者所有,如需转载,请联系作者。

原文链接:springlearn.blog.csdn.net/article/details/125876265

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。