MyBatis整体架构
Mybatis的功能架构分为三层
API接口层,数据处理层,基础支持层
API接口层
核心对象就是SqlSession,它是上层应用和Mybatis打交道的桥梁,也有的人称之为大门,SqlSession中定义了非常多的对数据库操作的方法,接口层在接受到调用请求时,会调用核心处理层的相应模块来完成具体的数据库操作
数据处理层
这一层主要就是跟数据库操作相关的动作都是在这数据处理层完成的。
核心处理层主要做了这4件事:
- 把接口中传入的参数解析并映射成JDBC类型;
- 解析xml文件中的SQL语句,包括插入参数和动态SQL的生成;
- 执行SQL语句;
- 处理结果集,并映射成Java对象。
插件也属于核心层,这是由它的工作方式和拦截的对象决定的。
基础支撑层
负责最基础的功能支撑,包括连接管理、事务管理、配置加载和缓存处理,这些都是共用的东西,将他们抽取出来作为最基础的组件。为上层的数据处理层提供最基础的支撑。
MyBatis的主要成员
Configuration
MyBatis所有的配置信息都保存在Configuration对象之中,配置文件中的大部分配置都会存储到该类中。
SqlSession
作为MyBatis工作的主要顶层API,表示和数据库交互时的会话,完成必要数据库增删改查功能。
Executor
MyBatis执行器,是MyBatis 调度的核心,负责SQL语句的生成和查询缓存的维护。
StatementHandler
封装了JDBC Statement操作,负责对JDBC statement 的操作,如设置参数等。
ParameterHandler
负责对用户传递的参数转换成JDBC Statement 所对应的数据类型。
ResultSetHandler
负责将JDBC返回的ResultSet结果集对象转换成List类型的集合。
TypeHandler
负责java数据类型和jdbc数据类型(也可以说是数据表列类型)之间的映射和转换。
MappedStatement
MappedStatement维护一条<select|update|delete|insert>节点的封装。
SqlSource
负责根据用户传递的parameterObject,动态地生成SQL语句,将信息封装到BoundSql对象中,并返回
BoundSql
表示动态生成的SQL语句以及相应的参数信息。
SqlSessionFactory的创建
SqlSessionFactory是通过SqlSessionFactoryBuilder工厂类创建的,而不是直接使用构造器。
SqlSessionFactoryBuilder的主要代码如下:
//SqlSessionFactoryBuilder是一个建造者模式
public SqlSessionFactory build(InputStream inputStream) {
return build(inputStream, null, null);
}
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.
}
}
}
public SqlSessionFactory build(Configuration config) {
return new DefaultSqlSessionFactory(config);
}
SqlSessionFactory提供了根据字节流、字符流以及直接使用org.apache.ibatis.session.Configuration配置类三种途径的读取配置信息方式,但究其根本都是首先将XML配置文件构建为Configuration配置类,然后将Configuration设置到SqlSessionFactory默认实现DefaultSqlSessionFactory的configurationz字段并返回。主要解析配置文件的逻辑都委托给XMLConfigBuilder了。
XMLConfigBuilder的主要代码
public class XMLConfigBuilder extends BaseBuilder {
....
/**
* Description 构造XMLConfigBuilder
* @param reader 流
* @param environment
* @param props
* @author songhongwei s19744
* @time 202/1/5 16:44
**/
public XMLConfigBuilder(Reader reader, String environment, Properties props) {
//XPathParser用于SAX解析xml,解析器硬编码为XMLMapperEntityResolver
this(new XPathParser(reader, true, props, new XMLMapperEntityResolver()), environment, props);
}
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
//创建Configuration对象,用来保存配置文件的配置
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
....
}
标签解析类
public class XPathParser {
private final Document document;
private boolean validation;
//XML标签解析器,可使用不通的解析器,通过构造方法传进来,使用到了策略模式
private EntityResolver entityResolver;
private Properties variables;
private XPath xpath;
/**
* Description 构造方法
*
* @param reader
* Reader
* @param validation
* 是否进行DTD 校验
* @param variables
* 属性配置
* @param entityResolver
* XML实体节点解析器
* @author songhongwei s19744
* @time 202/1/5 19:54
**/
public XPathParser(Reader reader, boolean validation, Properties variables, EntityResolver entityResolver) {
commonConstructor(validation, variables, entityResolver);
this.document = createDocument(new InputSource(reader));
}
//创建document
private Document createDocument(InputSource inputSource) {
// important: this must only be called AFTER common constructor
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setValidating(validation);
// 设置由本工厂创建的解析器是否支持XML命名空间 xmlns
factory.setNamespaceAware(false);
factory.setIgnoringComments(true);
factory.setIgnoringElementContentWhitespace(false);
// 设置是否将CDATA节点转换为Text节点
factory.setCoalescing(false);
// 设置是否展开实体引用节点,这里应该是sql片段引用的关键
factory.setExpandEntityReferences(true);
DocumentBuilder builder = factory.newDocumentBuilder();
// 设置解析mybatis 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 {
// NOP
}
});
return builder.parse(inputSource);
} catch (Exception e) {
throw new BuilderException("Error creating document instance. Cause: " + e, e);
}
}
}
XML解析
XMLConfigBuilder
以及解析Mapper文件的XMLMapperBuilder
都继承于BaseBuilder
。他们对于XML文件本身技术上的加载和解析都委托给了XPathParser
,最终用的是jdk自带的xml解析器而非第三方比如dom4j,底层使用了xpath方式进行节点解析。new XPathParser(reader, true, props, new XMLMapperEntityResolver())
的参数含义分别是Reader,是否进行DTD 校验,属性配置,XML实体节点解析器。
entityResolver,跟Spring的XML标签解析器一样,有默认的解析器,也有自定义的,主要使用了策略模式,在这里mybatis硬编码为XMLMapperEntityResolver。
XMLMapperEntityResolver的定义如下:
package org.apache.ibatis.builder.xml;
import org.apache.ibatis.io.Resources;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
/**
* Offline entity resolver for the MyBatis DTDs.
*
* @author Clinton Begin
* @author Eduardo Macarron
*/
public class XMLMapperEntityResolver implements EntityResolver {
private static final String IBATIS_CONFIG_SYSTEM = "ibatis-3-config.dtd";
private static final String IBATIS_MAPPER_SYSTEM = "ibatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_SYSTEM = "mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_SYSTEM = "mybatis-3-mapper.dtd";
private static final String MYBATIS_CONFIG_DTD = "org/apache/ibatis/builder/xml/mybatis-3-config.dtd";
private static final String MYBATIS_MAPPER_DTD = "org/apache/ibatis/builder/xml/mybatis-3-mapper.dtd";
/**
* Converts a public DTD into a local one. 将公共的DTD转换为本地模式
* @param publicId The public id that is what comes after "PUBLIC"
* @param systemId The system id that is what comes after the public id.
* @return The InputSource for the DTD
* @throws org.xml.sax.SAXException If anything goes wrong
*/
@Override
public InputSource resolveEntity(String publicId, String systemId) throws SAXException {
try {
if (systemId != null) {
String lowerCaseSystemId = systemId.toLowerCase(Locale.ENGLISH);
if (lowerCaseSystemId.contains(MYBATIS_CONFIG_SYSTEM) || lowerCaseSystemId.contains(IBATIS_CONFIG_SYSTEM)) {
return getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId);
} else if (lowerCaseSystemId.contains(MYBATIS_MAPPER_SYSTEM) || lowerCaseSystemId.contains(IBATIS_MAPPER_SYSTEM)) {
return getInputSource(MYBATIS_MAPPER_DTD, publicId, systemId);
}
}
return null;
} catch (Exception e) {
throw new SAXException(e.toString());
}
}
private InputSource getInputSource(String path, String publicId, String systemId) {
InputSource source = null;
if (path != null) {
try {
InputStream in = Resources.getResourceAsStream(path);
source = new InputSource(in);
source.setPublicId(publicId);
source.setSystemId(systemId);
} catch (IOException e) {
// ignore, null is ok
}
}
return source;
}
}
mybatis解析的时候,引用了本地的DTD文件,和本类在同一个package下,其中的ibatis-3-config.dtd应该主要是用于兼容用途。在其中getInputSource(MYBATIS_CONFIG_DTD, publicId, systemId)的调用里面有两个参数publicId(公共标识符)和systemId(系统标示符),他们是XML 1.0规范的一部分,在mybatis中,systemId指定的是mybatis-3-config.dtd和ibatis-3-config.dtd
调用栈
build:64, SqlSessionFactoryBuilder (org.apache.ibatis.session)
-->build:77, SqlSessionFactoryBuilder (org.apache.ibatis.session)
--><init>:89, XMLConfigBuilder (org.apache.ibatis.builder.xml)
--><init>:127, XPathParser (org.apache.ibatis.parsing)
-->createDocument:261, XPathParser (org.apache.ibatis.parsing)
-->parse:339, DocumentBuilderImpl (com.sun.org.apache.xerces.internal.jaxp)
-->parse:243, DOMParser (com.sun.org.apache.xerces.internal.parsers)
-->parse:141, XMLParser (com.sun.org.apache.xerces.internal.parsers)
-->parse:771, XML11Configuration (com.sun.org.apache.xerces.internal.parsers)
-->parse:842, XML11Configuration (com.sun.org.apache.xerces.internal.parsers)
-->scanDocument:505, XMLDocumentFragmentScannerImpl (com.sun.org.apache.xerces.internal.impl)
-->next:602, XMLDocumentScannerImpl (com.sun.org.apache.xerces.internal.impl)
-->next:959, XMLDocumentScannerImpl$PrologDriver (com.sun.org.apache.xerces.internal.impl)
-->next:1045, XMLDocumentScannerImpl$DTDDriver (com.sun.org.apache.xerces.internal.impl)
-->dispatch:1151, XMLDocumentScannerImpl$DTDDriver (com.sun.org.apache.xerces.internal.impl)
-->resolveEntityAsPerStax:997, XMLEntityManager (com.sun.org.apache.xerces.internal.impl)
-->resolveEntity:110, EntityResolverWrapper (com.sun.org.apache.xerces.internal.util)
-->resolveEntity:58, XMLMapperEntityResolver (org.apache.ibatis.builder.xml)
UML关系图
红色的线是调用关系
原图
这里主要是根据mybatis自身创建一个文档解析器(XMLMapperEntityResolver),然后调用parse将输入input source解析为DOM XML文档并返回。
再得到XPathParser实例之后,就调用另一个使用XPathParser作为配置来源的重载构造函数了
构造函数代码如下
private XMLConfigBuilder(XPathParser parser, String environment, Properties props) {
super(new Configuration());
ErrorContext.instance().resource("SQL Mapper Configuration");
this.configuration.setVariables(props);
this.parsed = false;
this.environment = environment;
this.parser = parser;
}
其中调用了父类BaseBuilder的构造器目的是设置类型别名注册器,以及类型处理器注册器;并设置关键配置environment以及properties文件。
Configuration配置生成
Configuration是个核心类xml配置文件里面的所有配置关键信息都用这个类表示,而且几乎所有的情况下都要依赖与Configuration这个类。XMLConfigBuilder
创建完成之后,SqlSessionFactoryBuild
调用parser.parse()
创建Configuration
,真正Configuration构建逻辑就在XMLConfigBuilder.parse()
里面
XMLConfigBuilder.parse
的代码如下:
public Configuration parse() {
// 判断是否重复解析
if (parsed) {
throw new BuilderException("Each XMLConfigBuilder can only be used once.");
}
parsed = true;
// 读取配置文件一级节点configuration,mybatis配置文件解析的主流程
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
config文件解析XMLConfigBuilder.parseConfiguration
。,parser.evalNode(“/configuration”)
返回根节点的org.apache.ibatis.parsing.XNode
表示,XNode里面主要把关键的节点属性和占位符变量结构化出来。然后调用parseConfiguration
根据mybatis的主要配置进行解析。
主要代码如下:
private void parseConfiguration(XNode root) {
try {
// properties 标签,用来配置参数信息,比如最常见的数据库连接信息
propertiesElement(root.evalNode("properties"));
Properties settings = settingsAsProperties(root.evalNode("settings"));
loadCustomVfs(settings);
loadCustomLogImpl(settings);
// 实体别名两种方式:1.指定单个实体;2.指定包
typeAliasesElement(root.evalNode("typeAliases"));
pluginElement(root.evalNode("plugins"));
// 插件
objectFactoryElement(root.evalNode("objectFactory"));
objectWrapperFactoryElement(root.evalNode("objectWrapperFactory"));
reflectorFactoryElement(root.evalNode("reflectorFactory"));
settingsElement(settings);
// 数据库环境
environmentsElement(root.evalNode("environments"));
databaseIdProviderElement(root.evalNode("databaseIdProvider"));
// 数据库类型和Java数据类型的转换
typeHandlerElement(root.evalNode("typeHandlers"));
// 这个是对数据库增删改查的解析
mapperElement(root.evalNode("mappers"));
} catch (Exception e) {
throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
}
}
所有的root.evalNode
底层都是调用XML DOM的evaluate()
方法,根据给定的节点表达式来计算指定的 XPath
表达式,并且返回一个XPathResult
对象,返回类型在Node.evalNode()
方法中均被强转为Node对象。
mybatis中所有环境配置、resultMap集合、sql语句集合、插件列表、缓存、加载的xml列表、类型别名、类型处理器等全部都维护在Configuration中。Configuration中包含了一个内部静态类StrictMap,它继承于HashMap,对HashMap的装饰在于增加了put时防重复的处理,get时取不到值时候的异常处理,这样核心应用层就不需要额外关心各种对象异常处理,简化应用层逻辑。
从设计上来说,我们可以说Configuration并不是一个thin类(也就是仅包含了属性以及getter/setter),而是一个rich类,它对部分逻辑进行了封装便于用户直接使用,而不是让用户各自散落处理,比如addResultMap方法和getMappedStatement方法等等
Configuration类的代码我就不在此处粘贴,有兴趣的可以访问https://mybatis.org/mybatis-3/zh/configuration.html#settings
所有我们在mybatis-config和mapper文件中使用的类似int/string/JDBC/POOLED等字面常量最终解析为具体的java类型都是在typeAliasRegistry构造器和Configuration构造器执行期间初始化的。这个可以在Configuration构造器和 protected final TypeAliasRegistry typeAliasRegistry = new TypeAliasRegistry()
属性中看到