title: Mybatis源码分析
date: 2021-02-17 11:21:28
tags: mybatis
description: Mybatis源码学习
(整体架构图)
(源码包架构图)
1.配置文件解析过程
根据配置文件构建SqlSessionFactory
String resource = "mybatis-config.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
我们首先会使用 MyBatis 提供的工具类 Resources 加载配置文件,得到一个输入流。然后再通过 SqlSessionFactoryBuilder 对象的 build 方法构建 SqlSessionFactory对象。这里的 build 方法是我们分析配置文件解析过程的入口方法.
build方法创建了配置文件解析器
public SqlSessionFactory build(InputStream inputStream,String environment,Properties properties){
//创建文件解析器
XMLConfigBuilder praser = new XMLConfigBuilder(inputStream, environment, properties);
//调用parse方法解析配置文件,生成Configuration对象
return build(parser.prase());
}
配置文件通过XMLConfigBuilder进行解析,下面是XMLCnfigBuilder的prase方法:
public Configuration parse(){
...
//解析配置
parseConfiguration(parser.evalNode("/configuration"));
return configuration;
}
xpath表达式/configuration代表的是配置文件中的结点,这里通过xpath选中这个结点,并传递给parseConfiguration方法:
private void parseConfiguration(XNode root){
//配置properties配置
propertiesElement(root.evalNode("properties"));
//解析settings配置,并将其转换为Properties对象
Properties settings = settingAsProperties(root.evalNode("settings"));
//加载vfs
loadCustomVfs(settings);
//解析typeAlases配置
typeAliasesElement(root.evalNode("typeAliases"));
//解析plugins配置
pluginElement(root.evalNode("plugins"));
...
}
1.1解析properties节点
properties节点配置
<properties resource="jdbc.properties">
<property name="jdbc.username" value="coolblog"/>
<property name="hello" value="world"/>
</properties>
节点解析主要包含三个步骤,一是解析节点的子节点,并将解析结果设置到 Properties 对象中。二是从文件系统或通过网络读取属性配置,这取决于节点的 resource 和 url 是否为空。最后一步则是将包含属性信息Properties 对象设置到XPathParser 和 Configuration 中。
1.2解析settings节点
配置节点
<settings>
<setting name="cacheEnabled" value="true"/>
<setting name="lazyLoadingEnabled" value="true"/>
<setting name="autoMppingBehavior" value="PARTAL"/>
</settings>
解析过程:
private Properties settingsAsProperties(XNode context){
//获取settings子节点中的内容
Properties props = context.getChildrenAsProperties();
//创建Configuration类的“元信息”对象
MetaClass metaConfig = MetaClass.forClass(Configuration.class,localReflectorFactory);
for(Object key : props.keySet()){
//监测Configuration中是否存在相关属性
}
}
- 解析 settings 子节点的内容,并将解析结果转成 Properties 对象
- 为 Configuration 创建元信息对象
- 通过 MetaClass 检测 Configuration 中是否存在某个属性的 setter 方法, 不存在则抛异常
- 若通过 MetaClass 的检测,则返回 Properties 对象,方法逻辑结束
1.3设置settings内容到Configuration中
settings节点内容解析出来后,可以将它存放到Configuration对象中,代码主要通过调用Configuration的setter方法
1.4解析typeAliases节点
配置包名的方式,让Mybatis扫描包中的类型,并根据类型得到相应的别名,可配合Alias注解使用,即通过注解为某个类配置别名
<typeAliases>
<package name="xyz.coolblog.chapter2.model1"/>
<package name="xyz.coolblog.chapter2.model2"/>
</typeAliases>
手动的方式,明确为某个类型配置别名
<typeAliases>
<typeAlias alias="article" type="xyz.coolblog.chapter2.model.Article"/>
<typeAlias alias="author" type="xyz.coolblog.chapter2.model.Author"/>
</typeAliases>
-
从节点中解析并注册别名
type属性是必须要配置的,而alias属性则不是必修的。如果使用者未配置alias属性,则需要mybatis自行为目标类型生成别名(使用类名的小写形式作为别名)。若别名为空,注册别名的任务交给registerAlias(Class<?>)方法处理,若不为空,则由registerAlias(String, Class<?>)进行别名注册
-
从指定的包中解析并注册别名
查找指定包下的所有类,遍历查找到的类型集合,为每个类型注册别名
- 通过 VFS(虚拟文件系统)获取指定包下的所有文件的路径名,
- 比如 xyz/coolblog/model/Article.class
- 筛选以.class 结尾的文件名
- 将路径名转成全限定的类名,通过类加载器加载类名
- 对类型进行匹配,若符合匹配规则,则将其放入内部集合中
简单说就是创建了一个Map<string,Class<?>>,解析mybatis的配置文件,将alias元素的值作为Map的key,通过反射机制获得的type元素对应的类名的类作为Map的value值,在真正使用时通过alias别名来获取真正的类。
1.5解析plugins节点
实现一个插件需要比简单,首先需要让插件类实现 Interceptor接口。然后在插件类上添加@Intercepts 和@Signature 注解,用于指定想要拦截的目标方法
<plugins>
<plugin interceptor="xyz.coolblog.mybatis.ExamplePlugin">
<property name="key" value="value"/>
</plugin>
</plugins>
插件的解析过程:获取配置,然后再解析拦截器类型,并实例化拦截器,最后向拦截器中设置属性,并将拦截器添加到Configuration中
1.6解析environments节点
mybatis中,事务管理器和数据源配置在environments节点中
<environments default="development">
<environment id="development">
<transactionManager type="JDBC"/>
<dataSource type="POOLED">
<property name="driver" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</dataSource>
</environment>
</environments>
解析过程:获取environments的default属性,environment节点的id要与父节点environments的属性default内容一致,解析transactionManager节点,解析dataSource节点,创建DataSource对象,创建Environment对象并设置到configuration中。
1.7解析typeHandlers节点
在向数据库存储或读取数据时,我们需要将数据库字段类型和Java类型进行转换。下面是类型处理器的配置方法:
<!--自动扫描-->
<!--自动扫描注册类型处理器时,应使用@MappedTypes和@MappedJdbcTypes注解配置javaType和jdbcType>
<typeHandlers>
<package name="xyz.coolblog.handlers"/>
</typeHandlers>
<!--手动配置-->
<typeHandlers>
<typeHandler jdbcType="TINYINT"
javaType="xyz.coolblog.constant.ArticleTypeEnum"
handler="xyz.coolblog.mybatis.ArticleTypeHandler"/>
</typeHandlers>
解析xml并调用不同的类型处理器注册方法
-
当javaTypeClass != null&&jdbcType != null时,即明确配置了javaType和jdbcType属性的值,调用register(Class,jdbcType,Class)进行注册,即把类型和处理器进行映射
private void register(Type javaType, JdbcType jdbcType,TypeHandler<?> handler){ if(javaType != null){ //jdbcType到TypeHandler的映射 Map<JdbcType, TypeHandler<?>> map = TYPE_HANDLER_MAP.get(javaType); if(map==null||map == NULL_TYPE_HANDLER_MAP){ map = new HashMap<JdbcType, TypeHandler<?>>(); //存储javaType到Map<JdbcType, TypeHandler>的映射 TYPE_HANDLER_MAP.put(javaType,map); } map.put(jdbcType,handler); } //存储所有的TypeHandler ALL_TYPE_HANDLERS_MAP.put(handler.getClass(),handler); }
-
当javaType != NULL&&JdbcType == null时,即仅设置了javaType的值,调用register(Class<?> javaTypeClass, Class<? > typeHandlerClass),主要做的是尝试从注解中获取JdbcType的值
-
当javaType == NULL&&JdbcType != null时,即仅设置了javaType的值,调用register(Class<? > typeHandlerClass),主要做的是尝试解析javaType的值
2.映射文件解析过程
Mybatis会在解析配置文件的过程中对映射文件进行解析,解析逻辑封装在mapperElement方法中
//XMLConfigBuilder
private void mapperElement(XNode parent) throws Exception{
if(parent!=null){
for(XNode child:parent.getChildren()){
if("package".equals(child.getName())){
//获取<package>节点中的name属性
String mapperPackage = child.getStringAttribute("name");
//从注定包中查找mapper接口,并根据mapper接口解析映射配置
configuration.addMappers(mapperPackage);
}else{
//获取resource/url/class等属性
//resource不为空,则从指定路径加载配置
...
}
}
}
}
上面代码的主要逻辑是遍历 mappers 的子节点,并根据节点属性值判断通过何种方式加载映射文件或映射信息。这里把配置在注解中的内容称为映射信息,以 XML 为载体的配置称为映射文件。在 MyBatis 中,共有四种加载映射文件或映射信息的方式。第一种是从文件系统中加载映射文件;第二种是通过 URL 的方式加载映射文件;第三种是通过 mapper 接口加载映射信息,映射信息可以配置在注解中,也可以配置在映射文件中。最后一种是通过包扫描的方式获取到某个包下的所有类,并使用第三种方式为每个类解析映射信息。
下面分析映射文件的解析过程,先看映射文件解析入口:
//XMLMapperBuilder
public void parse(){
//监测映射文件是否已被解析过
if(!configuration.isResourceLoaded(resource)){
//解析mapper节点
configurationElement(parser.evalNode("/mapper"));
//添加资源路径到“已解析资源集合"中
configuration.addLoadedResource(resource);
//通过命名空间绑定Mapper接口
bindMapperForNamespace();
//处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
}
映射文件解析入口逻辑包含三个核心操作,如下:
- 解析 mapper 节点
- 通过命名空间绑定 Mapper 接口
- 处理未完成解析的节点
2.1解析映射文件
<mapper namespace="xyz.coolblog.dao.AuthorDao">
<cache/>
<resultMap id="authorResult" type="Author">
<id property="id" colunn="id"/>
<result property="name" column="name"/>
<!--...-->
</resultMap>
<sql id="table">
author
</sql>
<select id="findOne" resultMap="authorResult">
SELECT
id,name,age,sex,email
FROM
<include refid="table"/>
WHERE
id=#{id}
</select>
</mapper>
以上配置中每种节点的解析逻辑都封装在了相应的方法中 ,这 些方法由 XMLMapperBuilder 类的configurationElement 方法统一调用。该方法的逻辑如下:
private void configurationElement(XNode context){
try{
//获取mapper命名空间
String namespace = context.getStringAttribute("namespace");
//异常处理
}
//设置命名空间到builderAssistant
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref节点
cacheRefElement(context.evalNode("cache-ref"));
//解析cache节点
cacheElement(context.evalNode("chche"));
//解析resultMap节点
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql节点
sqlElement(context.evalNodes("/mapper/sql"));
//解析select、delete等节点
buildStatementFromContext(context.evalNodes("select|insert|update|delete"));
}
2.2解析cache节点
mybatis提供两级缓存,一级缓存是SqlSession级别,默认为开启状态;二级缓存配置在映射文件中,使用者需要显示配置才能开启
<cache
eviction="FIFO"
flushInterval="60000"
size="512"
readOnly="true"/>
根据上面的配置创建出的缓存有以下特点:
- 按先进先出的策略淘汰缓存项
- 缓存的容量为 512 个对象引用
- 缓存每隔 60 秒刷新一次
- 缓存返回的对象是写安全的,即在外部修改对象不会影响到缓存内部存储对象
缓存配置解析:
2.3解析cache-ref节点
2.4解析resultMap节点
<!-- mybatis-config.xml 中 -->
<!--User类-->
<typeAlias type="com.someapp.model.User" alias="User"/>
<!-- SQL 映射 XML 中 -->
<select id="selectUsers" resultType="User">
select id, username, hashedPassword
from some_table
where id = #{id}
</select>
<!--显示配置resultMap-->
<resultMap id="userResultMap" type="User">
<id property="id" column="user_id" />
<result property="username" column="user_name"/>
<result property="password" column="hashed_password"/>
</resultMap>
-
获取resultMap节点的各种属性
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception { ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); // 获取 id 和 type 属性 String id = resultMapNode.getStringAttribute("id", resultMapNode.getValueBasedIdentifier()); String type = resultMapNode.getStringAttribute("type", resultMapNode.getStringAttribute("ofType", resultMapNode.getStringAttribute("resultType", resultMapNode.getStringAttribute("javaType")))); // 获取 extends 和 autoMapping String extend = resultMapNode.getStringAttribute("extends"); Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); // 解析 type 属性对应的类型 Class<?> typeClass = resolveClass(type); Discriminator discriminator = null; List<ResultMapping> resultMappings = new ArrayList<ResultMapping>(); resultMappings.addAll(additionalResultMappings);
-
遍历resultMap的子节点,并根据子节点名称执行相应的解析逻辑
// 获取并遍历 <resultMap> 的子节点列表 List<XNode> resultChildren = resultMapNode.getChildren(); for (XNode resultChild : resultChildren) { if ("constructor".equals(resultChild.getName())) { // 解析 constructor 节点,并生成相应的 ResultMapping processConstructorElement(resultChild, typeClass, resultMappings); } else if ("discriminator".equals(resultChild.getName())) { // 解析 discriminator 鉴别器节点 discriminator = processDiscriminatorElement( resultChild, typeClass, resultMappings); } else { List<ResultFlag> flags = new ArrayList<ResultFlag>(); if ("id".equals(resultChild.getName())) { // 添加 ID 到 flags 集合中 flags.add(ResultFlag.ID); } // 解析 id 和 property 节点,并生成相应的 ResultMapping resultMappings.add(buildResultMappingFromContext( resultChild, typeClass, flags)); } } ResultMapResolver resultMapResolver = new ResultMapResolver( builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
-
构建 ResultMap 对象
-
若构建过程中发生异常,则将 resultMapResolver 添加到
incompleteResultMaps 集合中try { // 根据前面获取到的信息构建 ResultMap 对象 return resultMapResolver.resolve(); } catch (IncompleteElementException e) { /* * 如果发生 IncompleteElementException 异常, * 这里将 resultMapResolver 添加到 incompleteResultMaps 集合中 */ configuration.addIncompleteResultMap(resultMapResolver); throw e; } }
-
解析id和result节点
-
resultMap 属性的解析过程
要相对复杂一些。该属性存在于和节点 -
<!--通过resultMap属性引用其他的resultMap节点--> <resultMap id="articleResult" type="Article"> <id property="id" column="id"/> <result property="title" column="article_title"/> <association property="article_author" column="article_author_id" resultMap="authorResult"/> </resultMap> <resultMap id="authorResult" type="Author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> </resultMap>
-
<!--采取resultMap嵌套的方式进行配置--> <resultMap id="articleResult" type="Article"> <id property="id" column="id"/> <result property="title" column="article_title"/> <association property="article_author" javaType="Author"> <id property="id" column="author_id"/> <result property="name" column="author_name"/> </association> </resultMap>
-