前言
📫作者简介:小明java问道之路,专注于研究计算机底层/Java/Liunx 内核,就职于大型金融公司后端高级工程师,擅长交易领域的高安全/可用/并发/性能的架构设计📫
🏆CSDN 专家博主/Java 领域优质创作者、阿里云专家博主、华为云享专家、51CTO 专家博主🏆
🔥如果此文还不错的话,还请👍关注、点赞、收藏三连支持👍一下博主~
本文导读
本文解析 MyBatis 工作流程源码,【总】可以把 MyBatis 的运行流程分为三大阶段:1. 初始化阶段;2. 代理封装阶段;3. 数据访问阶段;
一、MyBatis 工作原理(运行流程)
【总】可以把 MyBatis 的运行流程分为三大阶段:
1. 初始化阶段:读取 XML 配置文件和注解中的配置信息,创建配置对象,并完成各个模块的初始化的工作;
2. 代理封装阶段:封装 iBatis 的编程模型,使用 mapper 接口开发的初始化工作;
3. 数据访问阶段:通过 SqlSession 完成 SQL 的解析,参数的映射、SQL 的执行、结果的解析过程;
1、MyBatis 工作流程简述
(1)读取 MyBatis 配置文件:mybatis-config.xml 为 MyBatis 的全局配置文件,配置了 MyBatis 的运行环境等信息,例如数据库连接信息。
(2)加载映射文件。映射文件即 SQL 映射文件,该文件中配置了操作数据库的 SQL 语句,需要在 MyBatis 配置文件 mybatis-config.xml 中加载。mybatis-config.xml 文件可以加载多个映射文件,每个文件对应数据库中的一张表。
(3)构造会话工厂:通过 MyBatis 的环境等配置信息构建会话工厂 SqlSessionFactory。
(4)创建会话对象:由会话工厂创建 SqlSession 对象,该对象中包含了执行 SQL 语句的所有方法。
(5)Executor 执行器:MyBatis 底层定义了一个 Executor 接口来操作数据库,它将根据 SqlSession 传递的参数动态地生成需要执行的 SQL 语句,同时负责查询缓存的维护。
(6)MappedStatement 对象:在 Executor 接口的执行方法中有一个 MappedStatement 类型的参数,该参数是对映射信息的封装,用于存储要映射的 SQL 语句的 id、参数等信息。
(7)输入参数映射:输入参数类型可以是 Map、List 等集合类型,也可以是基本数据类型和 POJO 类型。输入参数映射过程类似于 JDBC 对 preparedStatement 对象设置参数的过程。
(8)输出结果映射:输出结果类型可以是 Map、 List 等集合类型,也可以是基本数据类型和 POJO 类型。输出结果映射过程类似于 JDBC 对结果集的解析过程。
二、MyBatis 工作流程源码分析
这部分我们要知道 MyBatis 初始化流程源码,MyBatis 是如何加载配置文件的? Mapper.xml 映射文件是如何解析的? SQL 语句是如何解析(<select>
、<insert>
、<delete>
、<update>
等 SQL 语句标签)?
在初始化的过程中,MyBatis 会读取 mybatis-config.xml 这个全局配置文件以及所有的 Mapper 映射.xml 配置文件,同时还会加载这两个配置文件中指定的类,解析类中的相关注解,最终将解析得到的信息转换成配置对象。完成配置加载之后,MyBatis 就会根据得到的配置对象初始化各个模块。
1、mybatis-config.xml 解析全流程
下面我们正式开始介绍 MyBatis 的初始化过程(工作过程、工作原理)。
MyBatis 初始化的第一个步骤就是加载和解析 mybatis-config.xml 这个全局配置文件,入口是 XMLConfigBuilder 这个 Builder 对象,它由 SqlSessionFactoryBuilder.build() 方法创建。XMLConfigBuilder 会解析 mybatis-config.xml 配置文件得到对应的 Configuration 全局配置对象,然后 SqlSessionFactoryBuilder 会根据得到的 Configuration 全局配置对象创建一个 DefaultSqlSessionFactory 对象返回给上层使用。
在 SqlSessionFactoryBuilder.build() 方法中也可以看到,XMLConfigBuilder.parse() 方法触发了 mybatis-config.xml 配置文件的解析,其中的 parseConfiguration() 方法定义了解析 mybatis-config.xml 配置文件的完整流程,核心步骤如下:
解析 <properties> 标签;
解析 <settings> 标签;
处理日志相关组件;
解析 <typeAliases> 标签;
解析 <plugins> 标签;
解析 <objectFactory> 标签;
解析 <objectWrapperFactory> 标签;
解析 <reflectorFactory> 标签;
解析 <environments> 标签;
解析 <databaseIdProvider> 标签;
解析 <typeHandlers> 标签;
解析 <mappers> 标签。
public SqlSessionFactory build(Reader reader, String environment, Properties properties) {
try {
XMLConfigBuilder parser = new XMLConfigBuilder(reader, environment, properties);
// 触发了 mybatis-config.xml 配置文件的解析
return build(parser.parse());
} catch (Exception e) {
throw ExceptionFactory.wrapException("Error building SqlSession.", e);
} finally {
ErrorContext.instance().reset();
try {
reader.close();
} catch (IOException e) {
// Intentionally ignore. Prefer previous error.
}
}
}
复制代码
这里创建的 XMLConfigBuilder 对象的核心功能就是解析 mybatis-config.xml 配置文件。XMLConfigBuilder 有一部分能力继承自 BaseBuilder 抽象类,具体继承关系如下图所示:
configuration(Configuration 类型):MyBatis 的初始化过程就是围绕 Configuration 对象展开的,我们可以认为 Configuration 是一个单例对象,MyBatis 初始化解析到的全部配置信息都会记录到 Configuration 对象中。typeAliasRegistry(TypeAliasRegistry 类型):别名注册中心。typeHandlerRegistry(TypeHandlerRegistry 类型):TypeHandler 注册中心。除了定义别名之外,我们在 mybatis-config.xml 配置文件中,还可以使用 <typeHandlers>
标签添加自定义 TypeHandler 实现,实现数据库类型与 Java 类型的自定义转换,这些自定义的 TypeHandler 都会记录在这个 TypeHandlerRegistry 对象中。
resolveAlias() 方法 :解析别名,核心逻辑是在中实现的,主要依赖于 TypeAliasRegistry 对象 resolveTypeHandler() 方法:解析 TypeHandler,主要依赖于 TypeHandlerRegistry 对象。
public abstract class BaseBuilder {
protected final Configuration configuration;
protected final TypeAliasRegistry typeAliasRegistry;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected Class<?> resolveAlias(String alias) {
return typeAliasRegistry.resolveAlias(alias);
}
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, String typeHandlerAlias) {
if (typeHandlerAlias == null) {
return null;
}
Class<?> type = resolveClass(typeHandlerAlias);
if (type != null && !TypeHandler.class.isAssignableFrom(type)) {
throw new BuilderException("Type " + type.getName() + " is not a valid TypeHandler because it does not implement TypeHandler interface");
}
@SuppressWarnings( "unchecked" ) // already verified it is a TypeHandler
Class<? extends TypeHandler<?>> typeHandlerType = (Class<? extends TypeHandler<?>>) type;
return resolveTypeHandler(javaType, typeHandlerType);
}
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
if (typeHandlerType == null) {
return null;
}
// javaType ignored for injected handlers see issue #746 for full detail
TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
if (handler == null) {
// not in registry, create a new one
handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
}
return handler;
}
}
复制代码
我们回来看 XMLConfigBuilder 这个 Builder 实现类,看看它是如何解析 mybatis-config.xml 配置文件的。
parsed(boolean 类型):状态标识字段,记录当前 XMLConfigBuilder 对象是否已经成功解析完 mybatis-config.xml 配置文件。parser(XPathParser 类型):XPathParser 对象是一个 XML 解析器,这里的 parser 对象就是用来解析 mybatis-config.xml 配置文件的。environment(String 类型): 标签定义的环境名称。localReflectorFactory(ReflectorFactory 类型):ReflectorFactory 接口的核心功能是实现对 Reflector 对象的创建和缓存。
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);
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);
}
}
}
复制代码
我们可以看到 parseConfiguration(XNode root) 就硬核处理,简单介绍几个常用的
处理<properties>
标签:从 <properties>
标签中解析出来的 KV 信息会被记录到一个 Properties 对象(也就是 Configuration 全局配置对象的 variables 字段),在后续解析其他标签的时候,MyBatis 会使用这个 Properties 对象中记录的 KV 信息替换匹配的占位符。
处理<settings>
标签:是否使用二级缓存、是否开启懒加载功能等,这些都是通过 mybatis-config.xml 配置文件中的 <settings>
标签进行配置的。XMLConfigBuilder.settingsAsProperties() 方法的核心逻辑就是解析 <settings>
标签,并将解析得到的配置信息记录到 Configuration 这个全局配置对象的同名属性中。
处理<mappers>
标签:<mappers>
标签中会指定 Mapper.xml 映射文件的位置,通过解析 <mappers>
标签,MyBatis 就能够知道去哪里加载这些 Mapper.xml 文件了。mapperElement() 方法就是 XMLConfigBuilder 处理 <mappers>
标签的具体实现,其中会初始化 XMLMapperBuilder 对象来加载各个 Mapper.xml 映射文件。同时,还会扫描 Mapper 映射文件相应的 Mapper 接口,处理其中的注解并将 Mapper 接口注册到 MapperRegistry 中。
2、Mapper.xml 映射文件解析全流程
在 mybatis-config.xml 配置文件中可以定义多个 <mapper>
标签指定 Mapper 配置文件的地址,MyBatis 会为每个 Mapper.xml 映射文件创建一个 XMLMapperBuilder 实例完成解析。
与 XMLConfigBuilder 类似,XMLMapperBuilder 也是具体构造者的角色,继承了 BaseBuilder 这个抽象类,解析 Mapper.xml 映射文件的入口是 XMLMapperBuilder.parse() 方法,其核心步骤如下:
1、执行 configurationElement() 方法解析整个 Mapper.xml 映射文件的内容;
2、获取当前 Mapper.xml 映射文件指定的 Mapper 接口,并进行注册;
3、处理 configurationElement() 方法中解析失败的 <resultMap> 标签;
4、处理 configurationElement() 方法中解析失败的 <cache-ref> 标签;
5、处理 configurationElement() 方法中解析失败的 SQL 语句标签。
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
private void configurationElement(XNode context) {
try {
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
cacheRefElement(context.evalNode("cache-ref"));
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
resultMapElements(context.evalNodes("/mapper/resultMap"));
sqlElement(context.evalNodes("/mapper/sql"));
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
} catch (Exception e) {
throw new BuilderException("Error parsing Mapper XML. Cause: " + e, e);
}
}
复制代码
configurationElement() 方法才是真正解析 Mapper.xml 映射文件的地方,其中定义了处理 Mapper.xml 映射文件的核心流程:获取 <mapper> 标签中的 namespace 属性,同时会进行多种边界检查;解析 <cache> 标签;解析 <cache-ref> 标签;解析 <resultMap> 标签;解析 <sql> 标签;解析 <select>、<insert>、<update>、<delete> 等 SQL 标签。
我们详细讲解下<cache> 标签和<resultMap> 标签 1、处理 <cache>
标签一级缓存是默认开启的,而二级缓存默认情况下并没有开启,如有需要,可以通过<cache>
标签为指定的 namespace 开启二级缓存。
XMLMapperBuilder 中解析 <cache> 标签的核心逻辑位于 cacheElement() 方法之中,其具体步骤如下:(1)获取 <cache> 标签中的各项属性(type、flushInterval、size 等属性);(2)读取 <cache> 标签下的子标签信息,这些信息将用于初始化二级缓存;(3)MapperBuilderAssistant 会根据上述配置信息,创建一个全新的 Cache 对象并添加到 Configuration.caches 集合中保存。CacheBuilder 是 Cache 的构造者,CacheBuilder 中最核心的方法是 build() 方法,其中会根据传入的配置信息创建底层存储数据的 Cache 对象以及相关的 Cache 装饰器,具体实现如下:
public Cache build() {
// 将implementation默认值设置为PerpetualCache,在decorators集合中默认添加LruCache装饰器,
// 都是在setDefaultImplementations()方法中完成的
setDefaultImplementations();
// 通过反射,初始化implementation指定类型的对象
Cache cache = newBaseCacheInstance(implementation, id);
// 创建Cache关联的MetaObject对象,并根据properties设置Cache中的各个字段
setCacheProperties(cache);
// 根据上面创建的Cache对象类型,决定是否添加装饰器
if (PerpetualCache.class.equals(cache.getClass())) {
// 如果是PerpetualCache类型,则为其添加decorators集合中指定的装饰器
for (Class<? extends Cache> decorator : decorators) {
// 通过反射创建Cache装饰器
cache = newCacheDecoratorInstance(decorator, cache);
// 依赖MetaObject将properties中配置信息设置到Cache的各个属性中,同时调用Cache的initialize()方法完成初始化
setCacheProperties(cache);
}
// 根据readWrite、blocking、clearInterval等配置,
// 添加SerializedCache、ScheduledCache等装饰器
cache = setStandardDecorators(cache);
} else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
// 如果不是PerpetualCache类型,就是其他自定义类型的Cache,则添加一个LoggingCache装饰器
cache = new LoggingCache(cache);
}
return cache;
}
复制代码
3、处理 resultMap 标签
MyBatis 提供了 <resultMap>
标签来定义结果集与 Java 对象之间的映射规则。
1、获取 <resultMap> 标签的 type 属性值,这个值表示结果集将被映射成 type 指定类型的对象。如果没有指定 type 属性的话,会找其他属性值,优先级依次是:type、ofType、resultType、javaType。在这一步中会确定映射得到的对象类型,这里支持别名转换。
2、解析<resultMap>标签下的各个子标签,每个子标签都会生成一个 ResultMapping 对象,这个 ResultMapping 对象会被添加到 resultMappings 集合(List<ResultMapping> 类型)中暂存。这里会涉及 <id>、<result>、<association>、<collection>、<discriminator> 等子标签的解析。
3、获取 <resultMap> 标签的 id 属性,默认值会拼装所有父标签的 id、value 或 property 属性值。
4、获取 <resultMap> 标签的 extends、autoMapping 等属性。
5、创建 ResultMapResolver 对象,ResultMapResolver 会根据上面解析到的 ResultMappings 集合以及 <resultMap> 标签的属性构造 ResultMap 对象,并将其添加到 Configuration.resultMaps 集合(StrictMap 类型)中。
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 创建 ResultMapResolver 对象
// ResultMapResolver会根据上面解析到的ResultMappings集合以及<resultMap>标签的属性构造 ResultMap 对象
// 并将其添加到 Configuration.resultMaps 集合(StrictMap 类型)中
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
复制代码
4、SQL 语句解析全流程
在 Mapper.xml 映射文件中,除了上面介绍的标签之外,还有一类比较重要的标签,那就是<select>、<insert>、<delete>、<update>等 SQL 语句标签。虽然定义在 Mapper.xml 映射文件中,但是这些标签是由 XMLStatementBuilder 进行解析的,而不再由 XMLMapperBuilder 来完成解析。
XMLStatementBuilder 解析 SQL 标签的入口方法 parseStatementNode() 方法,在该方法中首先会根据 id 属性和 databaseId 属性决定加载匹配的 SQL 标签,然后解析其中的<include> 标签和 <selectKey> 标签,相关的代码片段如下:
public void parseStatementNode() {
// 获取SQL标签的id以及databaseId属性
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 若databaseId属性值与当前使用的数据库不匹配,则不加载该SQL标签
// 若存在相同id且databaseId不为空的SQL标签,则不再加载该SQL标签
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 根据SQL标签的名称决定其SqlCommandType
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 获取SQL标签的属性值,例如,fetchSize、timeout、parameterType、parameterMap、
// resultMap、resultType、lang、resultSetType、flushCache、useCache等。
// 这些属性的具体含义在MyBatis官方文档中已经有比较详细的介绍了,这里不再赘述
... ...
// 在解析SQL语句之前,先处理其中的<include>标签
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
// 获取SQL标签的parameterType、lang两个属性
... ...
// 解析<selectKey>标签
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 前面是解析<selectKey>和<include>标签的逻辑,这里不再展示
// 当执行到这里的时候,<selectKey>和<include>标签已经被解析完毕,并删除掉了
// 下面是解析SQL语句的逻辑,也是parseStatementNode()方法的核心
// 通过LanguageDriver.createSqlSource()方法创建SqlSource对象
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 获取SQL标签中配置的resultSets、keyProperty、keyColumn等属性,以及前面解析<selectKey>标签得到的KeyGenerator对象等,
// 这些信息将会填充到MappedStatement对象中
// 根据上述属性信息创建MappedStatement对象,并添加到Configuration.mappedStatements集合中保存
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
}
复制代码
三、MyBatis 设计模式
MyBatis 在加载配置文件、创建配置对象的时候,会使用到经典设计模式中的建造者模式(Buildr Pattern),正文部分(谈谈你对 MyBatis 的理解【建议收藏】)也有提到,在这里我们详细聊一下
构造者模式的四个核心组件。
Product 接口:复杂对象的接口,定义了要创建的目标对象的行为。
ProductImpl 类:Product 接口的实现,它真正要创建的复杂对象,其中实现了我们需要的复杂业务逻辑。
Builder 接口:定义了构造 Product 对象的每一步行为。
BuilderImpl 类:Builder 接口的具体实现,其中具体实现了构造一个 Product 的每一个步骤,例如上图中的 setPart1()、setPart2() 等方法,都是用来构造 ProductImpl 对象的各个部分。在完成整个 Product 对象的构造之后,我们会通过 build() 方法返回这个构造好的 Product 对象。
使用构造者模式一般有两个目的。 第一个目的是将使用方与复杂对象的内部细节隔离,从而实现解耦的效果。使用方提供的所有信息,都是由 Builder 这个“中间商”接收的,然后由 Builder 消化这些信息并构造出一个完整可用的 Product 对象。 第二个目的是简化复杂对象的构造过程。在很多场景中,复杂对象可能有很多默认属性,这时我们就可以将这些默认属性封装到 Builder 中,这样就可以简化创建复杂对象所需的信息。
通过构建者模式的类图我们还可以看出,每个 BuilderImpl 实现都是能够独立创建出对应的 ProductImpl 对象,那么在程序需要扩展的时候,我们只需要添加新的 BuilderImpl 和 ProductImpl,就能实现功能的扩展,这完全符合“开放-封闭原则”
总结
本文解析 MyBatis 工作流程源码,MyBatis 的运行流程分为三大阶段 1. 初始化阶段;2. 代理封装阶段;3. 数据访问阶段
评论