# mybatis-3
**Repository Path**: topanda/mybatis-3
## Basic Information
- **Project Name**: mybatis-3
- **Description**: mybatis源码中文翻译,并包含学习时新增的单元测试
- **Primary Language**: Unknown
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 3
- **Forks**: 1
- **Created**: 2019-01-14
- **Last Updated**: 2024-05-29
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 菜鸟进阶记之MyBatis源码解析
## 找到学习Mybatis源码的切入点
如果我们采用硬编码的形式简单的使用Mybatis,代码大概会类似下面这种结构:
```
// 定义Mybatis 主要配置文件的地址
String resource = "resources/config-custom-s1.xml";
// 获取Mybatis配置文件的输入流
InputStream inputStream = Test.class.getClassLoader().getResourceAsStream(resource);
// 通过输入流构建SqlSessionFactory
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
// 通过SqlSessionFactory获取一个SqlSession
SqlSession sqlSession = sqlSessionFactory.openSession();
// 执行操作
// userDao 的方法将会被代理
UserDao userDao = sqlSession.getMapper(UserDao.class);
User teacher = userDao.getById(1);
System.out.println(userDao.insert(2, "panda"));
sqlSession.rollback();
// 关闭会话
sqlSession.close();
```
我们首先通过Mybatis的主要配置文件来初始化一个SqlSessionFactory的实例,之后通过SqlSessionFactory来获取一个SqlSession,然后在这个SqlSession中获取我们想要执行Dao操作的接口实例,进而执行DAO操作。
我们先忽略具体实现细节,先来了解这三个主要的类:
- SqlSessionFactoryBuilder
- SqlSessionFactory
- SqlSession
在我们常用的23种设计模式中,有一种设计模式叫做建造者模式,他通常用于构建复杂对象,在这里SqlSessionFactoryBuilder就是建造者模式的一种简单实现,他的作用是创建一个SqlSessionFactory对象实例。
事实上在Mybatis中,SqlSessionFactory的初始化是一个很复杂的过程,为了简化理解,我们暂且忽略掉其具体的初始化过程,仅需记住SqlSessionFactoryBuilder就是用来简化我们构造SqlSessionFactory对象复杂度一个工具类就可以了。
我们可以很容易的通过SqlSessionFactory这个名称,来简单的断定其是一个用于构造SqlSession的工厂类,那么说到工厂类就要提到设计模式中的工厂模式,简单的说工厂模式可以让我们使用工厂方法来代替常规new方法,其实也可以间接的简化一些构造过程,当然这里对设计模式只是稍微提及,并不会进行太深入的探讨,毕竟,学习Mybatis的源码才是我们的主要任务。
## SqlSession
SqlSessionFactory用来创建SqlSession对象,这个SqlSession对象就是我们需要了解的第一个真正的对象了。
我们都知道,如果我们要进行数据库操作,首先是要获取数据库链接,之后才能对数据库进行操作。
在Mybatis框架中,针对JDBC的操作进行了封装,统一了JDBC操作的APi,我们可以将这个API视为Mybatis框架中的Connection对象,这个API的具体表现形式就是SqlSession对象。
不过,区别于`Connection`,SqlSession对象除了封装了JDBC操作之外,还提供了其他好玩的东西,比如上文中调用的`SqlSession#getMapper()`方法,这个方法就很有意思,你传给他一个Dao操作接口的类型,他还给你一个对应的实体对象,之后你调用获取到的对象的方法的时候,就自带了JDBC效果。
那么这个看起来灰常六的功能是如何实现的呢?这就涉及到了Mybatis的核心原理,不过简单来讲,其实现依托了设计模式中的代理模式,代理模式也是常用的设计模式之一,它能够取代目标对象,代替目标对象执行操作流程,上文中自带的JDBC效果对于定义的DAO操作结果本质上不存在,但是因为代理对象的存在他假装成了一个DAO操作接口的实例,并完成了具有JDBC属性的操作。
但是Mybatis是在什么时候对Dao操作接口进行代理的呢?带着这个疑问,我们回到`SqlSessionFactoryBuilder`对象中,且看且分析。
## SqlSessionFactoryBuilder
`SqlSessionFactoryBuilder`看起来是一个很简单的接口,它定义了一个`build`方法,并为其提供了多种重载.
```
SqlSessionFactory build(Reader reader);
SqlSessionFactory build(Reader reader, String environment);
SqlSessionFactory build(Reader reader, Properties properties);
SqlSessionFactory build(Reader reader, String environment, Properties properties);
SqlSessionFactory build(InputStream inputStream);
SqlSessionFactory build(InputStream inputStream, String environment);
SqlSessionFactory build(InputStream inputStream, Properties properties);
SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);
SqlSessionFactory build(Configuration config);
```
在看源码是我们往往要看的是其参数最复杂的一个方法,因为通常情况下方法的调用会落在这个最复杂的方法上。
SqlSessionFactory的重载方法最复杂的有两个。
- 一个是`SqlSessionFactory build(InputStream inputStream, String environment, Properties properties);`
- 另一个是`SqlSessionFactory build(Reader reader, String environment, Properties properties);`
这两个方法其实很像,区别只是在于文件流的具体类型一个是Reader,一个是InputStream。
其余并无太大区别,所以这里我们只看InputStream对应的build方法,顺带一提的是,其实我们可以通过`Reader reader=new InputStreamReader(inputStream);`来将Inputstream转换为reader。
```
/**
* 创建一个SqlSessionFactory
*
* @param inputStream Mybatis配置文件对应的字节流
* @param environment 默认的环境ID,具体的environment的配置可以参考Mybatis配置文件内的Environments节点,默认为default
* @param properties 指定的配置
*/
public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {
try {
// 构建一个Mybatis 主要配置文件的解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 使用默认的JDBC会话工厂
return build(
parser.parse()/*解析Mybatis配置*/
);
} 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.
}
}
}
```
在build方法中,获取了一个XmlConfigBuilder的对象,这个对象实质上是抽象类`BaseBuilder`的一个具体实现,主要用来解析Mybatis的主要配置文件。
在定义上看`BaseBuilder`定义的目的应该是为了统一解析构建Mybatis组件的入口,但是其在具体实现中并没有遵循这一设计,而且个人不是很喜欢`*Builder`这个命名,因为这个命名听起来更像是用于构造某一复杂对象的,但是实际上他虽然负责构造了一些复杂对象,但是本质上更多的是解析XML的配置,所以个人还是比较喜欢`*Parser`这种命名方式,仅仅用来解析,具体的构建操作委托给`*Register`来实现,当然这只是个人喜好而已。
继续回到代码上来,当我们调用`XMLConfigBuilder#parse()`方法的时候,他会返回一个`Configuration`对象,并将`Configuration`对象传给方法`SqlSessionFactory build(Configuration config)`继续处理,好吧,打脸了,其最终实现竟然是调用了一个单参数方法。
```
/**
* 创建一个SqlSessionFactory
*
* @param config 指定的Mybatis配置类
*/
public SqlSessionFactory build(Configuration config) {
// 构建默认的SqlSessionFactory实例
return new DefaultSqlSessionFactory(config);
}
```
最终调用的这个方法很简单,只是通过`Configuration`构建了一个`DefaultSqlSessionFactory`实例而已。
我们先忽略`SqlSessionFactory build(Configuration config)`方法,将目光重新投向`XmlConfigBuilder`:
```
// 构建一个Mybatis 主要配置文件的解析器
XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties);
// 使用默认的JDBC会话工厂
return build(
parser.parse()/*解析Mybatis配置*/
);
```
这里调用了`XmlConfigBuilder`的`XMLConfigBuilder(InputStream inputStream, String environment, Properties props)`构造器来初始化一个`XmlConfigBuilder`实例对象,接着使用了`#parse`方法生成了一个`Configuration`对象用于配置Mybatis的`DefaultSqlSessionFactory`实例。
显而易见在`XmlConfigBuilder#parse()`中执行了一些不为我们所知的方法,它用来解析Mybatis的XMl配置文件的同时生成了一个Mybatis Configuration对象。
---
我们跟踪代码进入`XMLConfigBuilder# XMLConfigBuilder(InputStream inputStream, String environment, Properties props)`构造器中:
```
public XMLConfigBuilder(InputStream inputStream, String environment, Properties props) {
this(new XPathParser(
inputStream,/*XML文件输入流*/
true, /*开启验证*/
props, /*Property参*/
new XMLMapperEntityResolver() /*XML实体解析器*/
) /*新建一个XML解析器的实例*/
, environment/*环境对象*/
, props /*Property参数*/
);
}
```
在这里,Mybatis依据XML文件的输入流构建了一个`XpathParser`对象,`XpathParser`的作用很简单,它使用`SAX`解析出输入流对应的XML的DOM树,并存放到`XpathParser`对象中,部分`XpathParser`的代码如下:
```
public class XPathParser {
/**
* XML文本内容
*/
private final Document document;
/**
* 是否验证DTD
*/
private boolean validation;
/**
* 实体解析器
*/
private EntityResolver entityResolver;
/**
* 属性集合
*/
private Properties variables;
/**
* XML地址访问器
*/
private XPath xpath;
}
```
我们刚才调用的构造器如下:
```
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}
```
在这个构造器中有两个部分,一个是调用`commonConstructor`初始化当前的属性,另一个是调用`createDocument`来解析XMl文件对应的DOM树.
其中`commonConstructor`相对比较简单,代码如下:
```
/**
* 公共构造参数,主要是用来配置DocumentBuilderFactory
*
* @param validation 是否进行DTD校验
* @param variables 属性配置
* @param entityResolver 实体解析器
*/
private void commonConstructor(boolean validation, Properties variables, EntityResolver entityResolver) {
// 是否进行DTD校验
this.validation = validation;
// 配置XML实体解析器
this.entityResolver = entityResolver;
// Mybatis Properties
this.variables = variables;
// 初始化XPath
XPathFactory factory = XPathFactory.newInstance();
this.xpath = factory.newXPath();
}
```
`createDocument`方法则主要是根据当前的配置来进行解析Xml文件获取DOM对象:
```
/**
* 根据输入源,创建一个文本对象
*
* @param inputSource 输入源
* @return 文本对象
*/
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 是否验证被解析的文档
factory.setValidating(validation);
// 是否提供对XML命名空间的支持
factory.setNamespaceAware(false);
//忽略注释
factory.setIgnoringComments(true);
//忽略空白符
factory.setIgnoringElementContentWhitespace(false);
//是否解析CDATA节点转换为TEXT节点
factory.setCoalescing(false);
//是否展开实体引用节点
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
// 配置XML文档的解析器,现在主要是XMLMapperEntityResolver
builder.setEntityResolver(entityResolver);
// 配置错误处理器
builder.setErrorHandler(new ErrorHandler() {
@Override
public void error(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void fatalError(SAXParseException exception) throws SAXException {
throw exception;
}
@Override
public void warning(SAXParseException exception) throws SAXException {
}
});
// 解析出DOM树
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
```
在执行完这两个方法之后,`XPathParse`的构造过程就已经结束了,此时的他不仅持有`Document`对象还持有一个`Xpath`访问器,接下来很多操作都会使用`Xpath`来读取`Document`对象的内容。
我们继续回到`XMLConfigBuilder`的构造方法中,我们之前说过`XMLConfigBuilder`对象是`BaseBuilder`的子类,在`BaseBuilder`里有三个`final`修饰的属性:
```
public abstract class BaseBuilder {
/**
* Mybatis配置
*/
protected final Configuration configuration;
/**
* 类型别名注册表
*/
protected final TypeAliasRegistry typeAliasRegistry;
/**
* 类型转换处理器注册表
*/
protected final TypeHandlerRegistry typeHandlerRegistry;
```
- 其中`Configuration`对象是Mybaits的配置类,他里面封装了很多配置属性便于Mybatis在之后的处理过程中使用,我们暂且不提,等待具体使用时再去介绍。
- `TypeAliasRegistry`是Mybatis的类型别名注册表,其目的在于简化我们在使用Mybatis过程中的编码量,比如在我们在定义resultMap的result时,可以使用``,而不必非得使用类的全限定名称`java.lang.Integer`.
`TypeAliasRegistry`本身的实现是很简单的,他维护了一个叫做`Map> TYPE_ALIASES`的MAP集合,用来存储类别名和类定义的映射关系,并在其构造方法中默认注册了我们常用的一些类的别名。
- `TypeHandlerRegistry`属性维护了Mybatis的类型转换处理器注册表,类型转换处理器(`TypeHandler`)的作用是处理java类型和jdbc类型之间的互相转换工作,同样他也在构造时初始化注册了一些常用的类型转换器.
`XMLConfigBuilder`本身也定义了一些属性,其作用大致如下:
```
/*解析标志,防止重复解析*/
private boolean parsed;
/*XML地址解析器*/
private final XPathParser parser;
/*指定Mybatis当前运行使用的环境*/
private String environment;
```
这里`environment`属性的作用,在之后的解析过程中,我们会讲到。
回到刚才`XmlConfigBuilder`的构造器中,在完成初始化一个`XPathParse`对象之后,
会继续调用其重载的构造方法`XMLConfigBuilder(XPathParser parser, String environment, Properties props) `:
```
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
// 初始化Configuration对象的同时注册部分别名以及语言驱动
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
// 设置Mybatis变量
this.configuration.setVariables(props);
// 初始化解析标签
this.parsed = false;
// 初始化环境容器
this.environment = environment;
// 初始化Xml地址解析器
this.parser = parser;
}
```
需要注意的是,在这个构造器中,调用父类的构造器:
```
public BaseBuilder(Configuration configuration) {
this.configuration = configuration;
// 从Mybatis配置中同步过来类型别名注册表
this.typeAliasRegistry = this.configuration.getTypeAliasRegistry();
// 从Mybatis配置中同步过来类型处理器注册表
this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
}
```
通过代码我们可以看到`BaseBuilder#typeAliasRegistry`和`BaseBuilder#typeHandlerRegistry`都是引用的Mybatis `Configuration`中的属性,因为这里敲黑板!!!
> BaseBuilder#typeAliasRegistry和Configuration#typeAliasRegistry是同一个实例。
> BastBuilder#typeHandlerRegistry和Configuration#typeHandlerRegistry是同一个实例。
至此,整个`XMLConfigBuilder`的初始化过程也已经全部完成。
接下来,Mybvatis在`SqlSessionFactoryBuilder#build()`>`return build(parser.parse()/*解析Mybatis配置*/);`位置调用了`XmlConfigBuidler#parse()`方法触发了整个Mybatis的初始化解析过程。
```
public Configuration parse() {
if (parsed) {
// 第二次调用XMLConfigBuilder
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
// 重置XMLConfigBuilder的解析标志,防止重复解析
parsed = true;
// 此处开始进行Mybatis配置文件的解析流程
// 解析 configuration 配置文件,读取【configuration】节点下的内容
parseConfiguration(parser.evalNode("/configuration"));
// 返回Mybatis的配置实例
return configuration;
}
```
首先调用了方法` #parseConfiguration()`来解析Mybatis 主配置文件的 `configuration`节点,而`configuration`节点的数据获取使用了`parser.evalNode("/configuration")`,
该方法最终落点于方:
```
private Object evaluate(String expression, Object root, QName returnType) {
try {
// 在指定的上下文中计算XPath表达式,并将结果作为指定的类型返回。
return xpath.evaluate(expression, root, returnType);
} catch (Exception e) {
throw new BuilderException("Error evaluating XPath. Cause: " + e, e);
}
}
```
之后Mybatis将该对象通过方法`return new XNode(this, node, variables);`包装成一个XNode对象.
XNode节点的主要定义如下:
```
public class XNode {
/**
* DOM 节点
*/
private final Node node;
/**
* 当前节点的名称
*/
private final String name;
/**
* 节点内容
*/
private final String body;
/**
* 节点属性
*/
private final Properties attributes;
/**
* Mybatis对应的配置
*/
private final Properties variables;
/**
* XPathParser解析器
*/
private final XPathParser xpathParser;
}
```
其主要作用就是简化访问DOM资源的操作。
在将`configuration`节点的内容包装为XNode对象之后,就开始了真正的解析过程.
## 解析Mybatis的主配置文件,并配置Mybatis允许所需要的基础环境
`configuration`是Mybaits主配置文件的根节点,我们通常这样使用:
```
...
```
参考Mybatis的DTD定义文件,可以发现在`configuration`节点下允许出现11种类型的子节点,且都是可选的,这也意味着Mybatis针对这些属性都有默认的处理:
```
```
这些子节点的具体用途我们在接下来的解析过程中会一一说到。
还是继续回到`parseConfiguration`方法上:
```
/**
* 解析Configuration节点
*/
private void parseConfiguration(XNode root) {
try {
//issue #117 read properties first
// 加载资源配置文件,并覆盖对应的属性[properties节点]
propertiesElement(root.evalNode("properties"));
// 将settings标签内的内容转换为Property,并校验。
Properties settings = settingsAsProperties(root.evalNode("settings"));
// 根据settings的配置确定访问资源文件的方式
loadCustomVfs(settings);
// 根据settings的配置确定日志处理的方式
loadCustomLogImpl(settings);
// 别名解析
typeAliasesElement(root.evalNode("typeAliases"));
// 插件配置
pluginElement(root.evalNode("plugins"));
// 配置对象创建工厂
objectFactoryElement(root.evalNode("objectFactory"));
// 配置对象包装工厂
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
// 配置反射工厂
reflectorFactoryElement(root.evalNode("reflectorFactory"));
// 通过settings配置初始化全局配置
settingsElement(settings);
// read it after objectFactory and objectWrapperFactory issue #631
// 加载多环境源配置,寻找当前环境(默认为default)对应的事务管理器和数据源
environmentsElement(root.evalNode("environments"));
// 数据库类型标志创建类,Mybatis会加载不带databaseId以及当前数据库的databaseId属性的所有语句,有databaseId的
// 语句优先级大于没有databaseId的语句
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 注册类型转换器
typeHandlerElement(root.evalNode("typeHandlers"));
// !!注册解析Dao对应的MapperXml文件
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
```
### 解析配置Mybatis的属性资源(properties节点)
在解析的过程中,首先是加载`configuration -> properties`节点,并覆盖Mybatis Configuration中的variables属性。
properties属性的定义很简单:
```
```
它允许有多个`property`子项,同时也可以配置 `resource` 和`url` 属性,但是`resource`和`url`属性不能同时存在,
其中`property`子项的定义如下:
```
```
`property`是由`name`和`value`构成的,因此其可以被解析成Properties。
在具体的解析过程中,我们也可以看到Mybatis针对这三种不同的配置方式都给出了对应的解析操作。
```
/**
* 解析Properties标签
*
* @param context Properties节点
* @throws Exception 异常
*/
private void propertiesElement(XNode context) throws Exception {
if (context != null) {
// 获取Properties下得Property节点的name 和value值,并转换Properties属性
Properties defaults = context.getChildrenAsProperties();
// 获取引用的资源
String resource = context.getStringAttribute("resource");
String url = context.getStringAttribute("url");
if (resource != null && url != null) {
throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other.");
}
if (resource != null) {
// 加载*.Properties资源配置文件
defaults.putAll(Resources.getResourceAsProperties(resource));
} else if (url != null) {
// 加载*.Properties资源配置文件
defaults.putAll(Resources.getUrlAsProperties(url));
}
Properties vars = configuration.getVariables();
if (vars != null) {
// 加载配置内容
defaults.putAll(vars);
}
// 刷新配置内容,并将全局默认配置同步到解析器内,用于接下来的解析
parser.setVariables(defaults);
// 同步到Mybatis设置中
configuration.setVariables(defaults);
}
}
```
Mybatis首先获取`propeties`下的所有`proerty`子节点,并据此生成一个`Properties`实例,其中`context.getChildrenAsProperties();`的实现代码如下:
```
/**
* 获取settings 节点下的所有setting,setting>name作为Properties的key,
* setting>value 作为properties的value。
*/
public Properties getChildrenAsProperties() {
Properties properties = new Properties();
for (XNode child : getChildren()) {
String name = child.getStringAttribute("name");
String value = child.getStringAttribute("value");
if (name != null && value != null) {
properties.setProperty(name, value);
}
}
return properties;
```
之后会分别获取`propertie`s的`resource`和`url`两个属性,校验两者不得同时存在,之后根据`resource`/`url`加载对应的资源文件
并保存到生成的Properties对象中,之后会获取Mybatis 配置类(Configuration)中的所有变量,然后保存到Properties对象中,
最后使用聚合出的`Properties`对象刷新`XmlPathparser`中的Mybatis配置,然后刷新Mybatis 配置类(Configuration)的属性配置。
通过上诉的加载顺序我们不难理解针对Mybatis的属性配置,优先级由高到低依次为:
启动时设置的参数->Mybatis 主配置文件中properties的url/resource属性指向的配置 -> Mybatis 主配置文件中properties的property配置。
### 解析配置Mybatis的环境设置(settings节点)
在解析完`propeties`节点之后,继续解析`settings`节点。
```
// 将settings标签内的内容转换为Property,并校验。
Properties settings = settingsAsProperties(root.evalNode("settings"));
```
`settings`节点的定义如下:
```
```
他规定了在`settings`节点下必须有一个或者多个`setting`子节点,`setting`子节点必须有`name`和`value`两个参数
,且`setting`下不能继续有其他子节点。
从这个DTD上也不难看出,其实`settings`节点也会被转换为`Properties`对象。
解析`settings`的代码:
```
/**
* 解析settings节点
*
* @param context settings节点
*/
private Properties settingsAsProperties(XNode context) {
if (context == null) {
// 如果没有配置settings节点,返回个空配置
return new Properties();
}
Properties props = context.getChildrenAsProperties();
// 检查Settings节点下的配置是否被支持
// Check that all settings are known to the configuration class
// 获取Configuration的反射缓存类
MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);
for (Object key : props.keySet()) {
if (!metaConfig.hasSetter(String.valueOf(key))) {
// 校验setting 配置的属性是否支持
throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive).");
}
}
return props;
}
```
其实在解析`settings`的过程中做的事情比较少,第一步就是将整个`settings`节点转换为一个`Properties`对象,如果没有配置
`settings`节点,那么就返回一个空的properties对象,如果有的话,就检查现有的`settings`配置的属性是否全部被支持。
这里面就涉及到了一个新的对象,叫做`MetaClass`,`MetaClass`是Mybatis用于保存对象的类型的元数据定义的一个类。
它主要有两个属性,一个是反射工厂,一个是对象属性描述元数据类。
```
public class MetaClass {
/**
* 反射工厂
*/
private final ReflectorFactory reflectorFactory;
/**
* 指定类的元数据
*/
private final Reflector reflector;
}
```
其中`ReflectorFactory`用于读取`class`并生成`Reflector`的辅助性工厂类,`Reflector`则缓存了一个类定义的基本信息,包括
类的类型,可读可写属性名称,以及对应的getter/setter方法,构造函数等。
在这里`XmlConfigBuilder`使用`DefaultReflectorFactory`来获取了`Configuration`的基本属性,并判断`settings`配置的属性
在`Configuration`中是否有对应的setter方法。
至此,整个`settings`的解析已经完成。
接下来的操作,则是根据`settings`的配置,确定Mybatis访问jar内部资源的方式,在Mybatis中定义了一个`VFS`抽象类,这个
抽象类定义了访问容器资源的API,其默认有两个实现`JBoss6VFS.class, DefaultVFS.class`。
```
// 根据settings的配置确定访问资源文件的方式
loadCustomVfs(settings);
```
```
/**
* 加载访问系统虚拟文件系统的实现类
*
* @param props 系统全局配置
* @throws ClassNotFoundException 未找到实现类
*/
private void loadCustomVfs(Properties props) throws ClassNotFoundException {
// 获取配置的vfsImpl属性,如果存在覆盖默认的虚拟文件系统访问实现类
String value = props.getProperty("vfsImpl");
if (value != null) {
String[] clazzes = value.split(",");
for (String clazz : clazzes) {
if (!clazz.isEmpty()) {
@SuppressWarnings("unchecked")
Class extends VFS> vfsImpl = (Class extends VFS>) Resources.classForName(clazz);
// 更新Mybatis访问虚拟文件系统的实现类
configuration.setVfsImpl(vfsImpl);
}
}
}
}
```
在上述的代码中`configuration.setVfsImpl(vfsImpl)`并不是简单的设值而已,在方法中它调用了`VFS.addImplClass(this.vfsImpl);`:
```
/**
* 新增一个VFS实例
* @param vfsImpl VFS实例
*/
public void setVfsImpl(Class extends VFS> vfsImpl) {
if (vfsImpl != null) {
this.vfsImpl = vfsImpl;
// 添加一个新的VFS实例
VFS.addImplClass(this.vfsImpl);
}
}
```
`VFS.addImplClass(this.vfsImpl);`的作用是往`VFS#USER_IMPLEMENTATIONS`中添加一个`VFS`实现类,在获取VFS实例时会优先读取。
在完成更新Mybatis访问虚拟文件系统的实现类之后,会继续根据`settings`来完成Mybatis日志实现类的配置工作。
```
// 根据settings的配置确定日志处理的方式
loadCustomLogImpl(settings);
```
```
/**
* 加载自定义日志实现类
*
* @param props 全局配置
*/
private void loadCustomLogImpl(Properties props) {
Class extends Log> logImpl = resolveClass(props.getProperty("logImpl"));
configuration.setLogImpl(logImpl);
}
```
到这里,整个`settings`的处理才算是暂时告一段落。
### 解析使用Mybatis的别名机制(typeAliases节点)
接下来则是处理Mybatis的的类型别名注册的工作,关于类型别名,在文章前面提到过,它主要是用做简化我们使用Mybatis时的代码量的一个优化性操作。
在Mybatis中配置自己的别名,需要使用的元素是`typeAliases`,`typealiases`的定义如下:
```
```
在`typealiases`下允许有零个或多个`typeAlias`/`package`标签,且`typeAlias`和`package`均不允许再包含其他子元素。
其中:
- `typeAlias`下有两个可填参数,其中`type`指向一个java类型的权限定名称,且必填,`alias`属性表示该java对象的别名,非必填,默认是使用java类的`#getSimpleName()`.
- `package`下只有一个必填的参数`name`,该参数指向一个java包,包下的所有符合规则(默认是Object.class的子类)的类均会被注册。
```
/**
* 解析配置typeAliases节点
*
* @param parent typeAliases节点
*/
private void typeAliasesElement(XNode parent) {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
// 根据 package 来批量解析别名,默认为实体类的SimpleName
String typeAliasPackage = child.getStringAttribute("name");
// 注册别名映射关系到别名注册表
configuration.getTypeAliasRegistry().registerAliases(typeAliasPackage);
} else {
// 处理typeAlias配置,并将typeAlias>alias和typeAlias>type作为别名和类型注册到别名注册表
String alias = child.getStringAttribute("alias");
String type = child.getStringAttribute("type");
try {
Class> clazz = Resources.classForName(type);
if (alias == null) {
typeAliasRegistry.registerAlias(clazz);
} else {
typeAliasRegistry.registerAlias(alias, clazz);
}
} catch (ClassNotFoundException e) {
throw new BuilderException("Error registering typeAlias for '" + alias + "'. Cause: " + e, e);
}
}
}
}
}
```
Mybatis对于`typeAliases`的节点也是根据DTD定义的一样,分为了两种处理方式:一种是处理`package`子元素,另一种是处理`typeAlias`子元素。
其中针对`package`的处理,是首先获取`package`的`name`属性指向的基础包名,然后调用`configuration#getTypeAliasRegistry()#registerAliases()`方法来执行别名注册的操作。
> 这里有一点要注意,前文已经提到本质上在XmlConfigBuilder中的typeAliasRegistry和configuration中的typeAliasRegistry是同一实例,因此,此处调用`typeAliasRegistry#registerAliases()`和`configuration#getTypeAliasRegistry()#registerAliases()`对于最终结果来说并无实质区别。
然后看一下`TypeAliasRegistry#registerAliases(String)`方法,这个方法提供了批量注册类别名的功能。
```
/**
* 注册指定包下所有的java类
*
* @param packageName 指定包
*/
public void registerAliases(String packageName) {
registerAliases(packageName, Object.class);
}
```
调用其重载方法,并传入限制的类型,此处默认传入的是`Object.class`,因此其想要注册别名的类是该包下`Object.class`的子类集合。
```
/**
* 注册指定包下指定类型及其子实现的别名映射关系
*
* @param packageName 指定包名称
* @param superType 指定类型
*/
public void registerAliases(String packageName, Class> superType) {
ResolverUtil> resolverUtil = new ResolverUtil<>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
// 返回当前已经找到的类
Set>> typeSet = resolverUtil.getClasses();
for (Class> type : typeSet) {
// Ignore inner classes and interfaces (including package-info.java)
// Skip also inner classes. See issue #6
// 忽略匿名类、接口
if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) {
// 注册别名
registerAlias(type);
}
}
}
```
在这个方法中,他通过`ResolverUtils#find();`来获取初步符合预期的类集合,之后排除掉匿名类、成员类和接口,对剩下的类执行别名注册的操作。
> `ResolverUtils#find()`方法用于递归扫描指定的包及其子包中的类,并对所有找到的类执行Test测试,只有满足测试的类才会保留。
其代码如下:
```/**
* 递归扫描指定的包及其子包中的类,并对所有找到的类执行Test测试,只有满足测试的类才会保留。
*
* @param test 用于过滤类的测试对象
* @param packageName 被扫描的基础报名
*/
public ResolverUtil find(Test test, String packageName) {
String path = getPackagePath(packageName);
try {
// 获取指定路径下的所有文件
List children = VFS.getInstance().list(path);
for (String child : children) {
if (child.endsWith(".class")) {
// 处理下面所有的类编译文件
addIfMatching(test, child);
}
}
} catch (IOException ioe) {
log.error("Could not read package: " + packageName, ioe);
}
return this;
}
```
在上述方法中有一行代码`List children = VFS.getInstance().list(path);`,这里回顾到解析`settings`标签并配置`VFS`实例的时候说过,用户自定义的VFS实例就是在此处生效的.
```
/**
* 单例实例持有者
* Singleton instance holder.
*/
private static class VFSHolder {
static final VFS INSTANCE = createVFS();
@SuppressWarnings("unchecked")
static VFS createVFS() {
// Try the teacher implementations first, then the built-ins
List> impls = new ArrayList<>();
// 优先使用用户自己加载的
impls.addAll(USER_IMPLEMENTATIONS);
// 使用系统默认的
impls.addAll(Arrays.asList((Class extends VFS>[]) IMPLEMENTATIONS));
// Try each implementation class until a valid one is found
VFS vfs = null;
for (int i = 0; vfs == null || !vfs.isValid(); i++) {
// 当获取不到vfs对象时或者找到有效的vfs对象时结束.
// 获取vfs实例类型
Class extends VFS> impl = impls.get(i);
try {
// 实例化vfs
vfs = impl.newInstance();
if (vfs == null || !vfs.isValid()) {
if (log.isDebugEnabled()) {
log.debug("VFS implementation " + impl.getName() +
" is not valid in this environment.");
}
}
} catch (InstantiationException e) {
log.error("Failed to instantiate " + impl, e);
return null;
} catch (IllegalAccessException e) {
log.error("Failed to instantiate " + impl, e);
return null;
}
}
if (log.isDebugEnabled()) {
log.debug("Using VFS adapter " + vfs.getClass().getName());
}
return vfs;
}
}
```
对于`VFS`实例实例化的过程,其实也涉及到一种常用的设计模式 —— 单例模式,此处稍作了解即可。
继续回到别名解析的方法上来,刚才说到获取指定包下所有的需要被注册别名的类已经找到之后,Mybatis会继续处理这些类,将这些类的别名注册交给`registerAlias(Class)`方法来完成。
```
/**
* 注册指定类型的别名到别名注册表中
* 在没有注解的场景下,会使用的Bean实例的简短名称的首字母小写的名称作为别名
*
* @param type 指定类型
*/
public void registerAlias(Class> type) {
// 类别名默认是类的简单名称
String alias = type.getSimpleName();
// 处理注解中的别名配置
Alias aliasAnnotation = type.getAnnotation(Alias.class);
if (aliasAnnotation != null) {
alias = aliasAnnotation.value();
}
// 注册类别名
registerAlias(alias, type);
}
```
在这个方法里,主要是获取需要注册别名类的别名,别名的默认值是别名类通过`#getSimpleName()`方法获取到的简短名称,但是如果再别名类上找到注解`Alias`,`Alias`注解的别名将会覆盖默认别名。
在获取到别名之后,继续调用方法`registerAlias(String, Class>)`来真正执行注册别名的操作。
```
/**
* 将指定类型和别名注册到别名注册表
*
* @param alias 别名
* @param value 指定类型
*/
public void registerAlias(String alias, Class> value) {
if (alias == null) {
throw new TypeException("The parameter alias cannot be null");
}
// issue #748
String key = alias.toLowerCase(Locale.ENGLISH);
// 校验是否已经注册过
if (TYPE_ALIASES.containsKey(key) && TYPE_ALIASES.get(key) != null && !TYPE_ALIASES.get(key).equals(value)) {
throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + TYPE_ALIASES.get(key).getName() + "'.");
}
// 添加到别名映射表
TYPE_ALIASES.put(key, value);
}
```
注册别名的时候,首先将别名转换为全小写字符串,之后校验当前是否已经注册了同名称的别名或者同类型的别名,如果都没有的话才会将别名添加到别名注册表中,即`TypeAliasRegistry#TYPE_ALIASES`中。
到这儿,对于`typeAliases`中的`package`子标签的解析就完成了,接下来说的是另一个标签`typeAlias`,之前说过,`typeAlias`有两个属性,其中`type`属性是必填的,他使用类的全限定名称来指向一个java类,`alias`属性是非必填的,它用来指定java类的别名。
其实针对`typeAlias`的解析已经包含在了刚才对`package`解析的过程中了,在解析`typeAlias`标签时,首先会判断`type`属性指向的java类是否存在,如果不存在则会抛出异常,存在的话才会继续处理`alias`的值。
如果`typeAlias`指定了`alias`的值,他会调用`registerAlias(String, Class>)`方法来注册别名,如果没有指定`alias`的值,那么他会调用`registerAlias(Class)`方法在生成类别名之后,再通过`registerAlias(String, Class>)`方法注册别名.
到此,`typeAliases`节点也已经解析完成,接下来是解析Mybatis插件的过程。
### 解析使用Mybatis的插件机制(plugins节点)
Mybtis的插件是一个很强大的功能,它允许我们在Mybatis运行期间切入到Mybatis内部中,执行我们想要做的一些事情,比如比较火的分页插件`page-helper`其实就是基于Mybatis的插件功能实现的。
Mybatis的插件配置DTD定义如下:
```
```
`plugins`标签下必须定义一个或多个`plugin`标签,`plugin`有一个必填的属性`interceptor `,该值指向一个实现了`Interceptor`的类,同时在`plugin`标签下面运行出现零个或多个`property`标签,用来指定配置该插件的属性.
关于接口`Interceptor`的定义如下:
```
/**
* Mybatis 拦截器(插件)接口定义
* @author Clinton Begin
*/
public interface Interceptor {
/**
* 拦截代理类执行操作
* @param invocation 代理
*/
Object intercept(Invocation invocation) throws Throwable;
/**
* 拦截代理类的构建过程
*/
Object plugin(Object target);
/**
* 设置拦截器属性
*/
void setProperties(Properties properties);
}
```
然后我们继续看Mybatis插件配置的解析:
```
// 插件配置
pluginElement(root.evalNode("plugins"));
```
```
/**
* 解析plugins节点
*
* @param parent plugins节点
*/
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
// 处理每一个interceptor
String interceptor = child.getStringAttribute("interceptor");
// 获取name=>value映射
Properties properties = child.getChildrenAsProperties();
// 获取插件的实现
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
// 初始化插件配置
interceptorInstance.setProperties(properties);
// 注册插件,在插件职责链中注册一个新的插件实例
configuration.addInterceptor(interceptorInstance);
}
}
}
```
整个插件处理的过程中有两个地方需要注意,一个是`(Interceptor) resolveClass(interceptor).newInstance();`,另一个是`configuration.addInterceptor(interceptorInstance);`。
其中`resolveClass(String alias)`是在`BaseBuilder`中定义的一个方法,用于通过别名解析出一个具体的java类实例,因此这也意味着,我们可以通过别名的方式注册Mybatis插件.
`resolveClass(String alias)`方法的具体实现是委托给了`TypeAliasRegistry`的`resolveAlias(String)`来完成的.
```
public Class resolveAlias(String string) {
try {
if (string == null) {
return null;
}
// issue #748
String key = string.toLowerCase(Locale.ENGLISH);
Class value;
if (TYPE_ALIASES.containsKey(key)) {
// 优先从别名注册表加载
value = (Class) TYPE_ALIASES.get(key);
} else {
// 反射获取
value = (Class) Resources.classForName(string);
}
return value;
} catch (ClassNotFoundException e) {
throw new TypeException("Could not resolve type alias '" + string + "'. Cause: " + e, e);
}
}
```
`configuration.addInterceptor(interceptorInstance);`的实现也不是简单的赋值而是在现有的`Configuration#interceptorChain#interceptors`中添加一个新的`interceptor`,其中`Configuration#interceptorChain#interceptors`是一个列表。
`InterceptorChain`的定义如下:
```
/**
* Mybatis插件职责链
*
* @author Clinton Begin
*/
public class InterceptorChain {
/**
* 所有的插件
*/
private final List interceptors = new ArrayList<>();
/**
* 调用所有插件的{@link Interceptor#plugin(Object)}方法
*/
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
/**
* 添加一个新的{@link Interceptor#}实例
*/
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
/**
* 获取所有的Interceptor
*/
public List getInterceptors() {
return Collections.unmodifiableList(interceptors);
}
}
```
因此,对于拦截器的解析过程是首先解析出interceptor的类名(全限定名称或者别名),之后通过反射获取拦截器的对象实例,并将在配置文件中配置的`property`解析成`Properties`,调用拦截器的`setProperties(Properties)`方法完成赋值,最后注册到`Configuration#interceptorChain#interceptors`中。
至此,Mybatis的插件也已经完成解析,接下来要解析的就是对象创建工厂。
### 解析并配置Mybatis的对象创建工厂(objectFactory)
在Mybatis中其实有很多通过反射来实例化对象的操作,比如在JDBC操作完成之后将返回结果转换成具体的实体类的时候,对象的创建采用的就是反射来实现的,对于这种创建对象的操作,Mybatis提供了一个对象创建工厂的接口API,叫做`ObjectFactory`,其默认实现类是`DefaultObjectFactory`.
`ObjectFactory`类定义如下:
```
/**
* Mybatis用于所有对象的实例的工厂
*
* @author Clinton Begin
*/
public interface ObjectFactory {
/**
* 设置配置参数
*
* @param properties 配置参数
*/
void setProperties(Properties properties);
/**
* 使用默认的构造方法创建一个指定类型的实例
*
* @param type 指定类型
*/
T create(Class type);
/**
* 通过构造方法和构造参数创建一个指定类型的实例
* Creates a new object with the specified constructor and params.
*
* @param type Object type 指定对象类型
* @param constructorArgTypes 构造参数类型集合
* @param constructorArgs 构造参数集合
*/
T create(Class type, List> constructorArgTypes, List