Mybatis

第02篇:Mybatis配置文件解析

2026-01-295 min readJava 专题系列
Mybatis

一、配置文件分析

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

java
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 注意看高亮行

  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
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 元素 & 属性 & 属性值

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,(messagebody))>`
元素声明混合型的内容`<!ELEMENT note (#PCDATAto
属性属性声明<!ATTLIST 元素名称 属性名称 属性类型 默认值><!ATTLIST payment type CDATA "check">,payment有一个属性type,类型为字符类型,默认值check

<!ATTLIST 元素名称 属性名称 属性类型 默认值>

值类型

类型描述
CDATA值为字符数据 (character data)
(en1en2
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文件,我们知道有那些子标签及属性信息。内容比较长。但是不是很重要。这里只要知道就行。

后面我们看如何使用代码来解析这些标签。

2.3 Mybatis配置解析核心逻辑

!!!tip 思路决定出路

  • line(6) sqlSessionFactory.getConfiguration()

由此来看所有的解析都是在SqlSessionFactoryBuilder进行完成的. 具体的解析xml代码我们不研究,这里我们只要搞清楚它的调用关系,及实现的代码在哪里即可。如果这里 看懂,其实都会得到一个结论。就是mybaits的源码是比较简单的,因为他的配置是比较集中的,无论是xml方式或者是注解方式。 最终所有的配置信息都在 Configuration 类中。 !!!

java
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

java
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 重点关注

  1. line(8), 我们看到核心解析类是 XPathParser parser = new XPathParser()
  2. line(17), 标签的解析都在 parseConfiguration
  3. line(17), 思考下为什么先解析 propertiesElement(root.evalNode("properties")) !!!
java
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内部都有那些关键的配置类把。

属性解释
TypeAliasRegistrykey是一个别名,value是一个class对象
Properties variables配置文件中占位符的变量配置
InterceptorChain interceptorChain拦截链,用于拦截方法,实现插件
ObjectFactory objectFactory对象实例化统一的工厂方法,我们不用都反射来实例化了
ObjectWrapperFactory objectWrapperFactory包装对象后为其提供统一的属性操作方法。我们不用通过反射来修改对象属性值了
ReflectorFactory reflectorFactory反射工厂,用于生成一个反射信息对象,具有缓存的作用
Environment environment环境信息包含(事务管理器和数据源)
TypeHandlerRegistry typeHandlerRegistry主要处理jdbc的返回数据,转换成Java对象
MapperRegistry mapperRegistryMapper生成的处理类,包含代理的逻辑

2.3.4 Mapper.xml 解析

XMLMapperBuilder

解析Mapper对应的xml配置文件,这里面包含了sql的信息。

mapper的dtd约束文件更多,可以参考: https://mybatis.org/mybatis-3/zh/sqlmap-xml.html#

xml
1 <!-- 映射文件,mybatis精髓, 后面才会细讲 --> 2 <mappers> 3 <mapper resource="mapper/TUserMapper.xml"/> 4 </mappers>

这里就要介绍一个重要的类的,MapperBuilderAssistant Mapper构建辅助工具类。

属性解释
MapperBuilderAssistantMapper构建辅助工具类(缓存配置)
CacheRefResolver决定如何使用缓存
ParameterMapping当sql中使用到了#{}占位符时候,负责填充sql参数
ResultMapResolver返回值映射
Map<String, XNode> sqlFragmentssql片段
MappedStatementMapper方法的所有信息(出参,入参,及sql信息等)

2.4 Mybatis可以借鉴的知识点

2.4.1 占位符解析逻辑

在第一篇的时候我们说过,从配置文件解析中我们能学会,如果解析占位符。并将占位符填充真实数据。这里我们就具体说下是如何解析。 还记得前面让思考下为什么先解析 propertiesElement(root.evalNode("properties"))

答案就是为了先读取变量信息,方便后面给依赖的信息,给填充值。

我们直接说答案: 具体谁来做了这个事情,从职责划分上来看,这个其实还是属于xml文件解析。所以是 XPathParser parser XPathParser中填充上变量信息,这样XPathParser在解析的时候会自动将 ${} 填充上真实的数据。

java
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 对象

java
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 占位符解析

java
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();

java
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}

java
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

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