个人博客导航页(点击
右侧
链接
即可打开个人博客):
大牛带你入门技术栈
Ext1:本文源码解析基于 mybatis-spring-boot-starter 2.1.1,即 mybatis 3.5.3 版本。
Ext2:本文主要是对源码的讲解,着重点会是在源码上。
一、从 MybatisAutoConfiguration 说开去,mapper 文件是怎么扫描的?
我们知道配置
SqlSessionFactory
是我们集成
Mybatis
时需要用到的常客,
SqlSessionFactory
顾名思义是用来创建
SqlSession
对象的,
SqlSession
对象的重要程度不言而喻。源码中提到,
SqlSession
是
Mybatis
运行最重要的一个接口,通过此接口,我们可以进行我们的操作指令,获取
mapper
,管理事务等操作。
官网
给出了一个简单的配置demo,通过
SqlSessionFactoryBean
进行
sqlSessionFactory
的创建。
@Bean
public SqlSessionFactory sqlSessionFactory() {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
factoryBean.setDataSource(dataSource());
return factoryBean.getObject();
}
我们可以拿到这个
SqlSessionBean
来进行我们一些定制化操作,比如
mybatis插件
,自定义的返回处理等等。如果我们不显式声明
SqlSessionFactory
,则会使用 mybatis-spring-boot-autoconfigure 下的这个
bean
的注册:
我们可以看到在
mybatis
里的很多定制化常客,都出现在了这里。比如,配置 mapper 文件位置的配置,我们用以下的小段代码来看的话:
-- 来自代码 org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration#sqlSessionFactory --
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
它的实现实际上非常简单:拿到我们所有的
mapperLocations
这个数组,解析成
Resource
数组。
-- 来自代码 org.mybatis.spring.boot.autoconfigure.MybatisProperties#resolveMapperLocations --
public Resource[] resolveMapperLocations() {
return Stream.of(Optional.ofNullable(this.mapperLocations).orElse(new String[0]))
.flatMap(location -> Stream.of(getResources(location))).toArray(Resource[]::new);
}
private Resource[] getResources(String location) {
try {
return resourceResolver.getResources(location);
} catch (IOException e) {
return new Resource[0];
}
}
-- application.yml中的配置 --
mybatis:
mapper-locations: classpath*:com/anur/mybatisdemo/test/mapper/*.xml
*/
纵览一下这几者的关系,
SqlSessionFactory
是根据配置
Configuration
与
sqlSessionFactoryBuilder
共同创建的,如果在 spring 项目中,则会由
SqlSessionFactoryBean
来替代
SqlSessionFactoryBuilder
进行创建。
二、SqlSessionFactory 的初始化与 XMLMapperBuilder
其实上面扯了那么多,只是想引入一下
XMLMapperBuilder
。我们知道,我们的配置(比如Spring中的 application.yml),最后会被解析成
Configuration
,而
mapper.xml
文件正是依据我们的配置来进行读取的,读取到的 xml 将被读取成
Resource
文件,最后在
SqlSessionFactoryBean
初始化完毕后、也就是在创建
SqlSessionFactory
之前:
会通过
XMLMapperBuilder
完成 xml 文件的解析。
XMLMapperBuilder
在完成初始化后,调用
org.apache.ibatis.builder.xml.XMLMapperBuilder#parse
来进行真正的 mapper 文件解析:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
configurationElement
就是对我们
xml
文件的解析,通过
parser.evalNode("/mapper")
拿到我们编写的
xml
的
<mapper>
标签进行初步的解析,源码如下:可以看到许多熟悉的身影,比如
namespace
、
resultMap
、
select|insert|update|delete
之类的。
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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
给一个简单的
xml
看一下
mapper
标签里面的内容方便理解,就是 <mapper xxxxxxxxx> </mapper> 之间那一大段内容,
mybaits
封装的这套
XNode
可以使得我们访问
xml
像访问
map
一样轻松:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.anur.mybatisdemo.test.TrackerConfigMapper">
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
select *
from tracker_config
where in_use = 1
<if test="followerId != null">and user_id = #{followerId}</if>
</select>
<select id="getFollower" resultType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
select *
from tracker_config
where in_use = 1
<if test="followerId != null">and user_id = #{followerId}</if>
limit 1
</select>
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="user_d" property="userId"/>
<result column="in_use" property="inUse"/>
<association property="config" resultMap="customMap"/>
</resultMap>
</mapper>
三、ResultMap 是如何解析的
方才说到,
configurationElement()
方法负责对
xml
文件进行解析,我们拿几个主要的元素出来讲讲,比如
resultMap
:
resultMapElements(context.evalNodes("/mapper/resultMap"));
就是解析
resultMap
的入口,同样的,先拿到
resultMap
这个 XML 节点,进入到
resultMapElements
这个方法,
resultMapElements
负责解析
xml
,最后,将解析的结果交给
ResultMapResolver
处理。
我们先忽略
ResultMapResolver
,简单看看
resultMapElement
中做了什么,对应的源码如下,大体可分为两类解析:
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
Class<?> typeClass = resolveClass(type);
if (typeClass == null) {
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<>();
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<>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
String extend = resultMapNode.getStringAttribute("extends");
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
-
一种是对
resultMap
本身属性的解析,也就是
getStringAttribute
,例如当前
resultMap
的
type
是什么,它是否开启
autoMapping
,
id
是什么之类的。 -
一种则是对子标签的解析,子标签的解析,则分为
constructor
、
discriminator
、以及其他字段的解析。
如图所示:
3.1 ResultMap 中的重要成员:typeHandler
在
mybatis
对
mysql
返回的结果集
resultSet
进行解析时,
typeHandler
有着举足轻重的作用。
mysql
的
JdbcType
有很多,比如
BLOB
,
VARCHAR
,
DATE
等等,而我们的 java 类型(
mybatis
称之为
javaType
,或者
javaTypeClass
)也很多,还包括我们很多的 自定义的
TypeHandler
,这里就不赘述了。
那么必然存在一个问题,如何将它们一一对应上?毫无疑问,
JdbcType
可以被解析为多个
javaTypeClass
,如
VARCHAR
可以对应解析成我们的
JSON JAVA BEAN
,也可以解析为
String
等等;同样,
String
类型也可以由多个
JdbcType
解析而来,比如
DATE
类型可以经过一定规则的解析,成为
String
类型的时间。
答案就在
org.apache.ibatis.type.TypeHandlerRegistry
。
public TypeHandlerRegistry() {
register(Boolean.class, new BooleanTypeHandler());
register(boolean.class, new BooleanTypeHandler());
register(JdbcType.BOOLEAN, new BooleanTypeHandler());
register(JdbcType.BIT, new BooleanTypeHandler());
register(Byte.class, new ByteTypeHandler());
register(byte.class, new ByteTypeHandler());
register(JdbcType.TINYINT, new ByteTypeHandler());
register(Short.class, new ShortTypeHandler());
register(short.class, new ShortTypeHandler());
register(JdbcType.SMALLINT, new ShortTypeHandler());
register(Integer.class, new IntegerTypeHandler());
register(int.class, new IntegerTypeHandler());
register(JdbcType.INTEGER, new IntegerTypeHandler());
register(Long.class, new LongTypeHandler());
register(long.class, new LongTypeHandler());
register(Float.class, new FloatTypeHandler());
register(float.class, new FloatTypeHandler());
register(JdbcType.FLOAT, new FloatTypeHandler());
register(Double.class, new DoubleTypeHandler());
register(double.class, new DoubleTypeHandler());
register(JdbcType.DOUBLE, new DoubleTypeHandler());
........
TypeHandlerRegistry
中注册了许多
javaTypeClass
->
JdbcType
的映射,内部维护了一个变量
private final Map<Type, Map<JdbcType, TypeHandler<?>>> typeHandlerMap = new ConcurrentHashMap<>();
当我们获取某个
TypeHandler
时,先根据
javaTypeClass
获取到
Map<JdbcType, TypeHandler<?>
,然后再根据
JdbcType
获取到具体的
TypeHandler
。
例如,对于
javaTypeClass
:
java.util.Date
来说,默认有三种映射,分别是:
null -> DateTypeHandler
"TIME" -> TimeOnlyTypeHandler
"DATE" -> DateOnlyTypeHandler
源码中,优先根据
jdbcType
获取,如果获取不到,则使用兜底的配置,也就是默认的
TypeHandler
,代码如下:
private <T> TypeHandler<T> getTypeHandler(Type type, JdbcType jdbcType) {
if (ParamMap.class.equals(type)) {
return null;
}
Map<JdbcType, TypeHandler<?>> jdbcHandlerMap = getJdbcHandlerMap(type);
TypeHandler<?> handler = null;
if (jdbcHandlerMap != null) {
handler = jdbcHandlerMap.get(jdbcType); // 优先根据 `jdbcType` 获取
if (handler == null) {
handler = jdbcHandlerMap.get(null);// 否则获取默认的,key 为 null
}
if (handler == null) {
// #591
handler = pickSoleHandler(jdbcHandlerMap);
}
}
// type drives generics here
return (TypeHandler<T>) handler;
}
3.2 mapper.xml 配置与 typeHandler
上面只是说了
typeHandler
的获取,那么又是如何从
mapper
文件解析出我们需要的
typeHandler
呢?这里直接上结论,再一一解析。
上图看起来好像复杂,实际上解析过程十分简单,遵循以下几个获取顺序:
-
优先获取 xml 配置的
typeHandler
,自己配置的
typeHandler
优先级最高 -
若果没有配置,则需要从刚才讲的那个
TypeHandlerRegistry
中,通过
javaTypeClass
+
jdbcType
获取 -
如果
javaTypeClass
为空则使用
Object.class
类型作为
javaTypeClass
-
如果
jdbcType
为空则获取默认的
typeHandler
javaTypeClass
也有自己的获取权重,顺序如下:
-
优先获取 xml 配置的
javaType
-
否则根据
property
+
resultType
根据反射来获取
3.2.1 typeHandler 的获取顺序
源码也很容易看明白,先通过 xml 文件获取
typeHandler
== org.apache.ibatis.builder.xml.XMLMapperBuilder#buildResultMappingFromContext ==
String typeHandler = context.getStringAttribute("typeHandler");
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
比如对于下面这个xml,update_time 这一属性,会优先使用
Date2StrTypeHandler
来进行解析。
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="user_id" property="userId"/>
<result column="update_time" property="updateTime" typeHandler="com.anur.mybatisdemo.Date2StrTypeHandler"/>
<result column="in_use" property="inUse"/>
<association property="config" resultMap="customMap"/>
</resultMap>
紧接着,如果 xml 中指定了
typeHandler
,则创建一个
typeHandler
实例,如果没有指定,则
typeHandler
会在下一步骤进行创建。
== org.apache.ibatis.builder.BaseBuilder#resolveTypeHandler(java.lang.Class<?>, java.lang.Class<? extends org.apache.ibatis.type.TypeHandler<?>>) ==
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;
}
没有手动指定
TypeHandler
,那么则会使用
javaTypeClass
+
JdbcType
共同来定位一个
TypeHandler
,也就是调用
typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType)
这个方法,此方法在本文 2.1.1 有提到过
== org.apache.ibatis.mapping.ResultMapping.Builder#resolveTypeHandler ==
private void resolveTypeHandler() {
if (resultMapping.typeHandler == null && resultMapping.javaType != null) {
Configuration configuration = resultMapping.configuration;
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
resultMapping.typeHandler = typeHandlerRegistry.getTypeHandler(resultMapping.javaType, resultMapping.jdbcType);
}
}
3.2.2 javaTypeClass 的获取顺序
javaTypeClass
也是一样的道理,先是优先从 xml 中获取:
String javaType = context.getStringAttribute("javaType");
Class<?> javaTypeClass = resolveClass(javaType);
比如下面这个 xml 的 userId 这一属性,
javaTypeClass
就是
Integer
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="user_id" property="userId" javaType="java.lang.Integer"/>
<result column="update_time" property="updateTime" typeHandler="com.anur.mybatisdemo.Date2StrTypeHandler"/>
<result column="in_use" property="inUse"/>
<association property="config" resultMap="customMap"/>
</resultMap>
但是如果我们不指定,源码中则是这么处理的,通过
property
这个 xml 配置,配合我们的
resultType
,共同进行解析,还是拿上面那个 xml 为例, in_use 这个属性,由于我们没有指定
javaType
,它会通过
resultMap
中我们指定的那个 javaBean ,也就是 TrackerConfigDO 连同
property = inUse
,通过反射来进行解析。
private Class<?> resolveResultJavaType(Class<?> resultType, String property, Class<?> javaType) {
if (javaType == null && property != null) {
try {
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
javaType = metaResultType.getSetterType(property);
} catch (Exception e) {
//ignore, following null check statement will deal with the situation
}
}
if (javaType == null) {
javaType = Object.class;
}
return javaType;
}
3.3 ResultMap 中的另一个常用属性: resultMap嵌套
resultMap
嵌套,包括使用
association
一对一的关联、
collection
一对多的管理与
discriminator
+
case
的魔幻sql语句 (感觉这么写很蛋疼)
resultMap
嵌套大体分为两种情况
-
其一是指定直接嵌套,比如下面所示 xml 中的
collection
标签 -
其二是指定另一个
resultMap
进行嵌套,如下所示
association
标签 - (或者上面两种互相嵌套组合)
3.3.1 resultMap嵌套中的两种解析规则
3.3.1.1 其一,如果指定了显式的
resultMap
,则直接拿到它的名字
这种情况,内嵌
ResultMap
十分简单,就是直接拿到名字:
String nestedResultMap = context.getStringAttribute("resultMap",
// 下面会运行,但是不会生成的 id 和当前的 nestedResultMap 没关系,因为我们指定了 resultMap
processNestedResultMappings(context, Collections.emptyList(), resultType));
最后,如果你没指定
resultMap
是哪个包来的,则会给你加上前面的
namespace
public String applyCurrentNamespace(String base, boolean isReference) {
if (base == null) {
return null;
}
if (isReference) {
// is it qualified with any namespace yet?
if (base.contains(".")) {
return base;
}
} else {
// is it qualified with this namespace yet?
if (base.startsWith(currentNamespace + ".")) {
return base;
}
if (base.contains(".")) {
throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
}
}
return currentNamespace + "." + base;
}
例如指定了一个一对一关联:
<association property="config" resultMap="customMap"/>
,我们拿到的
id
不是
customMap
,而是
com.anur.mybatisdemo.test.TrackerConfigMapper.customMap
这种情况比较简单,这里就不赘述了(注意,这种情况也会递归解析(下面的这一小节)此标签,但是名字拿的是我们指定的名字)。
3.3.1.2 其二,如果未指定显式
resultMap
,则递归解析,拿到其 ValueBasedIdentifier ,即 id
它的递归解析上大体如下图所示:
// 方法一,如何解析一个 resultMap,以及其子节点
== org.apache.ibatis.builder.xml.XMLMapperBuilder#resultMapElement(org.apache.ibatis.parsing.XNode, java.util.List<org.apache.ibatis.mapping.ResultMapping>, java.lang.Class<?>) ==
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) throws Exception {
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {// 调用方法二循环解析子节点
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
return resultMapResolver.resolve();
}
// 方法二,如何解析一个子节点,如果子节点中包含 resultMap,或者 association、collection、case 等,调用方法三
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) throws Exception {
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.emptyList(), resultType));
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
// 方法三,调用方法一
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) throws Exception {
if ("association".equals(context.getName())
|| "collection".equals(context.getName())
|| "case".equals(context.getName())) {
if (context.getStringAttribute("select") == null) {
validateCollection(context, enclosingType);
ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
return resultMap.getId();
}
}
return null;
}
用图表示则为以下三个方法:
为了避免看起来很混乱,
下面将第一个解析
resultMap
标签的方法称为
resultMap解析方法
,
将第二个解析子标签
resultMapping
的方法称为
子标签解析方法
,
将第三个判断子标签有无内嵌
resultMap
如果有,则调用第一个方法的方法称为
内嵌解析方法
还是拿出我们的 xml 文件来举栗子:
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<association property="config" resultMap="customMap"/>
<collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="role" property="role"/>
<result column="in_use" property="inUse"/>
</collection>
</resultMap>
-
首先调用
resultMap解析方法
解析我们当前的最外层,即
customMap
,id为
xxx略xxx.mapper_customMap
-
发现有子标签,遍历子标签
-
子标签
<collection>
存在内嵌
resultMap
-
调用
resultMap解析方法
解析其内嵌
resultMap
(略) -
子标签
<collection>
由于显式指定了
resultMap
,所以其
内嵌id
为
xxx略xxx.mapper_customMap
-
调用
-
子标签
<association>
没有显式指定
resultMap
,故其内嵌id 从调用
resultMap解析方法
中来-
调用
resultMap解析方法
解析其内嵌
resultMap
-
发现有子标签,遍历子标签
-
子标签
role
没有内嵌
resultMap
-
子标签
inUse
没有内嵌
resultMap
-
子标签
-
解析完毕,此
resultMap
Id
为
xxx略xxx.mapper_resultMap[customMap]_collection[configDOList]
-
子标签
<association>
没有显式指定
resultMap
,
内嵌id
为
xxx略xxx.mapper_resultMap[customMap]_collection[configDOList]
-
调用
-
子标签
3.3.2 ValueBasedIdentifier
刚才提到的
内嵌id
,或者
id
实际上是“一个东西”,可以理解为地址和引用之间的关系。比如说,这个
resultMap
的
id
叫做
customMap
,它的子标签中内嵌了一个
resultMap
,
内嵌id
为
customMap
。
从上面的解析我们也可以看出,我们的
ResultMap
只有持有一层结构,即使,
ResultMap
持有其所有子标签
resultMapping
,而子标签
resultMapping
对另外
ResultMap
,是通过记录其
id
的形式持有的。
这些
ValueBasedIdentifier
或者
id
,生成规则如下:
public String getValueBasedIdentifier() {
StringBuilder builder = new StringBuilder();
XNode current = this;
while (current != null) {
if (current != this) {
builder.insert(0, "_");
}
String value = current.getStringAttribute("id",
current.getStringAttribute("value",
current.getStringAttribute("property", null)));
if (value != null) {
value = value.replace('.', '_');
builder.insert(0, "]");
builder.insert(0,
value);
builder.insert(0, "[");
}
builder.insert(0, current.getName());
current = current.getParent();
}
return builder.toString();
}
代码很好理解,优先获取标签的
id
属性、其次则是
value
属性、最后是
property
属性, 如果不为空,替换一下
.
符号,避免它把你命名里面的
.
当成路径来解析,然后在左右套一个
[]
(
insert(0,val)
+
while
相当于递归 + 按顺序
append
,逻辑也更清楚)
总结规则为
_标签名[命名(可能为空)]
最后再在前面塞一个当前标签的标签名,然后
while
循环上层来向更上层命名。
也就是说打个比方,下面的 xml 最里层的这个关联标签会生成一个
mapper_resultMap[customMap]_collection[configDOList]_association[config]
的
ValueBasedIdentifier
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<association property="config" resultMap="customMap"/>
</collection>
</resultMap>
3.3.3 ResultMap 解析总结
其实到现在已经很明了了,所有的
ResultMap
都会被生成一个独立的数据结构,所以无论怎么嵌套,起码在解析层面,是不会出问题的,它只会保存自己的所有子标签,用
List<ResultMapping>
表示,如果子标签中存在内嵌的
ResultMap
,则仅仅保存其
id
,并另外(递归)解析此
ResultMap
比如下面这个xml的解析结果:
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<association property="config" resultMap="customMap"/>
<collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="role" property="role"/>
<result column="in_use" property="inUse"/>
</collection>
</resultMap>
如此图所示,虚线不是真正的关联,只是保存了一个叫做
nestedResultMapId
的属性,即
内嵌id
3.4 在解析 resultSet(查询结果集) 时是如何实现的,会不会死循环?
我们已经对
resultMap
的解析建立起了清晰的认知,那么此时还有另外一个问题,
mybatis
在对查询结果集进行解析的时候,是如何使用
resultMap
的?
虽然此部分与
mapper.xml
无关,但如果无法建立起体系,单纯的
resultMap
分析只会让人一头雾水。
实际上
在
resultMap
解析完成后,mybatis 会将其保存在
configuration
中
。
configuration
前面也提到过,里面保存了 mybatis 的配置,但它不仅如此,它还承担了我们
mybtais
上下文对象的作用。类似于
spring
框架中的
applicationContext
。
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
configuration.addResultMap(resultMap);
目光来到我们的
ResultHandler
,我们知道
ResultHandler
是
SqlSession
,也是 mybatis 的核心组件之一,它负责对
ResultSet
进行解析。
解析的核心代码如下(有所删减,后续会有文章专门分析
ResultSetHandler
,所以这里只是简单提一下)
private Object (ResultSetWrapper rsw, ResultMap resultMap, CacheKey combinedKey, String columnPrefix, Object partialObject) throws SQLException {
final String resultMapId = resultMap.getId(); // 这个id 就是我们前面说了很久的那个 ValueBasedIdentifier
Object rowValue = partialObject;
if (rowValue != null) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);
ancestorObjects.put(resultMapId, resultObject);
applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, false);
ancestorObjects.remove(resultMapId);
} else {
final ResultLoaderMap lazyLoader = new ResultLoaderMap();
rowValue = createResultObject(rsw, resultMap, lazyLoader, columnPrefix); // 根据反射构建出当前resultMap的承载对象
if (rowValue != null && !hasTypeHandlerForResultObject(rsw, resultMap.getType())) {
final MetaObject metaObject = configuration.newMetaObject(rowValue);// metaObject 是 mybatis 对对象的一套类似反射的封装,但不仅仅是反射这么简单
boolean foundValues = this.useConstructorMappings;
foundValues = applyPropertyMappings(rsw, resultMap, metaObject, lazyLoader, columnPrefix)
|| foundValues;// 解析普通的子标签属性
ancestorObjects.put(resultMapId, resultObject);
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
|| foundValues; // 解析内嵌 resultMap
ancestorObjects.remove(resultMapId);
foundValues = lazyLoader.size() > 0 || foundValues;
rowValue = foundValues || configuration.isReturnInstanceForEmptyRow() ? rowValue : null;
}
if (combinedKey != CacheKey.NULL_CACHE_KEY) {
nestedResultObjects.put(combinedKey, rowValue);
}
}
return rowValue;
}
// 解析内嵌 resultMap
private boolean applyNestedResultMappings(ResultSetWrapper rsw, ResultMap resultMap, MetaObject metaObject, String parentPrefix, CacheKey parentRowKey, boolean newObject) {
boolean foundValues = false;
for (ResultMapping resultMapping : resultMap.getPropertyResultMappings()) {
final String nestedResultMapId = resultMapping.getNestedResultMapId();
if (nestedResultMapId != null && resultMapping.getResultSet() == null) { // 如果子标签有内嵌resultMap,才会继续解析
try {
final String columnPrefix = getColumnPrefix(parentPrefix, resultMapping);
final ResultMap nestedResultMap =
getNestedResultMap(rsw.getResultSet(), nestedResultMapId, columnPrefix);// 拿到内嵌的resultMap
if (resultMapping.getColumnPrefix() == null) {
// try to fill circular reference only when columnPrefix
// is not specified for the nested result map (issue #215)
// 解决循环引用的核心代码
Object ancestorObject = ancestorObjects.get(nestedResultMapId);
if (ancestorObject != null) {
if (newObject) {
linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
}
continue;
}
}
Object rowValue = nestedResultObjects.get(combinedKey);
boolean knownValue = rowValue != null;
instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject); // mandatory
if (anyNotNullColumnHasValue(resultMapping, columnPrefix, rsw)) {
final CacheKey rowKey = createRowKey(nestedResultMap, rsw, columnPrefix);
final CacheKey combinedKey = combineKeys(rowKey, parentRowKey);
// 套娃递归~~
rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
if (rowValue != null && !knownValue) {
linkObjects(metaObject, resultMapping, rowValue);
foundValues = true;
}
}
} catch (SQLException e) {
throw new ExecutorException("Error getting nested result map values for '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
}
}
return foundValues;
}
对我们的对象进行赋值,实际上就是来来回回调用这两个方法,那么它如何防止resultMap套自己引起的无限解析呢?
3.4.1 借助额外的 Map映射 来解决 resultMap 套娃
<resultMap id="customMap" type="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<association property="config" resultMap="customMap"/>
<collection property="configDOList" ofType="com.anur.mybatisdemo.test.pojo.TrackerConfigDO">
<result column="role" property="role"/>
<result column="in_use" property="inUse"/>
</collection>
</resultMap>
实际上很简单,我们的
resultMap
都有一个唯一的
id
,也就是我们所提到的
ValueBasedIdentifier
,在第一次进入到
getRowValue
方法时,会通过反射创建我们的
resultMap
所表示的对象,比如上面
customMap
这个
resultMap
,会创建一个
TrackerConfigDO
对象,然后在解析内嵌
resultMap
之前,有一个关键动作,将创建的对象放进
ancestorObjects
:
ancestorObjects.put(resultMapId, resultObject);
foundValues = applyNestedResultMappings(rsw, resultMap, metaObject, columnPrefix, combinedKey, true)
|| foundValues; // 解析内嵌 resultMap
是的,就是
ancestorObjects
这个map映射,在解析内嵌
resultMap
子标签时,发现 config 这个子标签所指向的
resultMap
是它的父亲,也就是 config 这个子标签所引用的
resultMap
的
ValueBasedIdentifier
与外面
id = customMap
的相同,它会把我们刚才放进
ancestorObjects
里的那个对象拿出来,然后直接continue,不再继续向下解析了。
也就是打个比方
TrackerConfigDO@9999
这个对象中,有一个成员变量叫
config
,它也指向
TrackerConfigDO@9999
。
// try to fill circular reference only when columnPrefix
// is not specified for the nested result map (issue #215)
// 解决循环引用的核心代码
Object ancestorObject = ancestorObjects.get(nestedResultMapId);
if (ancestorObject != null) {
if (newObject) {
linkObjects(metaObject, resultMapping, ancestorObject); // issue #385
}
continue;
}
如果发现不是同一个
ValueBasedIdentifier
,则是一个递归解析,它会递归调用刚才的
resultMap
解析方法:
rowValue = getRowValue(rsw, nestedResultMap, combinedKey, columnPrefix, rowValue);
和前面说的三个方法递归很像,就是它把解析的主体定义为一个
resultMap
,在
resultMap
的子标签中如果发现了内嵌
resultMap
,则进行递归,我这里就不啰嗦了。
Extra:
既然说到了这个
ancestorObjects
map,顺便提一嘴。我们知道,一对多的映射,比如一条主数据对应10条从数据,在 mysql 中查询出来时,实际上是有10条的。是我们 mybatis 把这十条合成了一条,那么它是怎么做的呢?
实际上也是通过
ancestorObject
来完成的,不过这里主要讲的是
mapper
部分相关源码,后续会有文章专门讲
ResultSetHandler
四、sql语句与 mappedStatment
饶了一大圈,视线回到我们的
XMLMapperBuilder
:
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. The XML location is '" + resource + "'. Cause: " + e, e);
}
}
同样是拿到我们的 insert,update 等节点们:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
它的主要方法就是
statementParser.parseStatementNode();
,构造函数里面什么都没有,就是对几个成员变量进行赋值,这里就不啰嗦了。
我们看到
parseStatementNode()
,代码很长,但是我们不着急,现将它拆解成几个部分:
- 1、属性获取,大部分代码实际上都是在对属性(attr)进行获取,比如 resultMap,resultType之类的。
- 2、对内嵌语句 sqlFragment、SelectKey 的解析
- 3、生成 SqlSource
- 4、创建 mappedStatment
第一部分太简单,这里不啰嗦
。可以理解为给你一个map,或者json,各种获取值,把它存起来,除此之外没别的了,这里主要对后面几个部分进行讲解。
public void parseStatementNode() {
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
boolean useCache = context.getBooleanAttribute("useCache", isSelect);
boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);
// Include Fragments before parsing
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
String parameterType = context.getStringAttribute("parameterType");
Class<?> parameterTypeClass = resolveClass(parameterType);
String lang = context.getStringAttribute("lang");
LanguageDriver langDriver = getLanguageDriver(lang);
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
Integer fetchSize = context.getIntAttribute("fetchSize");
Integer timeout = context.getIntAttribute("timeout");
String parameterMap = context.getStringAttribute("parameterMap");
String resultType = context.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
String resultMap = context.getStringAttribute("resultMap");
String resultSetType = context.getStringAttribute("resultSetType");
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
if (resultSetTypeEnum == null) {
resultSetTypeEnum = configuration.getDefaultResultSetType();
}
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
String resultSets = context.getStringAttribute("resultSets");
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
4.1 sql 内嵌语句 sqlFragment
这两个标签相信大家都熟悉,就是提高sql复用率(个人不喜欢这种写法)。有了前面
resultMap
的基础,我们很容易猜得到,这些标签在解析完以后,会生成自己一个唯一的
id
,然后存到
configuration
里面。
是的,这里也确实是这么操作的。
首先,
sqlFragment
的解析代码如下:
== org.apache.ibatis.builder.xml.XMLIncludeTransformer#applyIncludes(org.w3c.dom.Node, java.util.Properties, boolean) ==
/**
* Recursively apply includes through all SQL fragments.
* @param source Include node in DOM tree
* @param variablesContext Current context for static variables with values
*/
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
if (source.getNodeName().equals("include")) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
} else if (included && (source.getNodeType() == Node.TEXT_NODE || source.getNodeType() == Node.CDATA_SECTION_NODE)
&& !variablesContext.isEmpty()) {
// replace variables in text node
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
我们一步步解析,这段代码有三个大的分支
-
第一个分支顾名思义,解析
include
标签用的, - 第二个则是解析普通节点用的,
-
第三个 else 则是代表解析
Text
节点、
CDATASection
节点。
先看看怎么解析普通节点,普通节点将节点拆成子节点,然后循环递归调用自己,这个没什么好说的,前面已经说了各种递归,这个也是一样的道理,
mybatis
解析套娃,核心就是递归。
text
节点的解析也很简单,唯一值得注意的就是
variablesContext
这个东西,它实际上是从配置文件中来,我们可以通过配置全局
variables
,它会在这个时候将其填充进去。
打个比方,下面这个
testValue
,我们在配置里面将其指定为全局变量,在写sql时,会将配置的值注入(不推荐)。
mybatis:
configuration-properties:
testValue: 43
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
select role,in_use, id as inner_id from tracker_config where id <= ${testValue}
</select>
后面两个解析很简单,主要是我们的第一个分支,就是如何去解析
include
标签:
看到我们这块分支的代码,上来第一步,是根据名字去
configuration
拿
include
,这个很好理解。紧接着就是一个递归,直接忽略它,前面的几个小结讲了太多递归,这里懒得再讲了。
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);// 递归解析 include标签
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);// 进行资源的引入
}
source.getParentNode().replaceChild(toInclude, source);// 将sql进行替换
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);// 移出引入sql的外标签
我们还是将其分为四个步骤
- 1、递归调用,解析 include 标签里面可能含有的 include 标签
- 2、判断 include 元素与当前这个节点是否是同一个文件, 如果不是同一个文件,则将其引入。这个没什么好说的 = =,一些 xml 的 api
-
3、将include标签进行替换,也就是
source.getParentNode().replaceChild(toInclude, source);
,也就是将真正的那段sql移过来。 -
4、一个while循环 +
toInclude.getParentNode().removeChild(toInclude);
,这部分实际上就是将刚才移过来那段sql的外层标签去掉,内容(childNode)拿出来。(主要是它没有 removeNodeWrapper(开玩笑的)这种方法,所以它这里采取了一种让人疑惑的写法)
我们拿这样的一段 xml 来模拟一下这个过程:
<sql id="select">select</sql>
<sql id="including">
<include refid="select"/>
*
from tracker_config
</sql>
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
<include refid="including"/>
where id <= ${hashmap.id}
</select>
解析
id = getAllFollower
这个节点,它是一个普通节点,所以循环它的所有子节点,第一个子节点就是
<include refid="including"/>
,首先根据
refid
拿到真正的资源,先给阿姨倒一杯卡布奇诺,先给即将引入的这段 sql 来一个解析套餐。
解析
id="including"
,一样的规则,它是个普通节点,循环它所有子节点,第一个子节点是
<include refid="select"/>
,和上面一样,即将引入的这另一段 sql 也会被奉上解析套餐。
解析套餐三大分支都没什么可以对
<sql id="select">select</sql>
做的,于是进入到我们的 include 四个步骤的后续步骤,先是将其替换,如下所示:
<sql id="select">select</sql>
<sql id="including">
<sql id="select">select</sql>
*
from tracker_config
</sql>
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
<include refid="including"/>
where id <= ${hashmap.id}
</select>
然后来到第四步,也就是去“头“,如下:
<sql id="select">select</sql>
<sql id="including">
select
*
from tracker_config
</sql>
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
<include refid="including"/>
where id <= ${hashmap.id}
</select>
最里层的递归出栈,来到上层递归,也就是
<include refid="including"/>
的解析,一样的,先替换:
<sql id="select">select</sql>
<sql id="including">
select
*
from tracker_config
</sql>
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
<sql id="including">
select
*
from tracker_config
</sql>
where id <= ${hashmap.id}
</select>
然后去头:
<sql id="select">select</sql>
<sql id="including">
select
*
from tracker_config
</sql>
<select id="getAllFollower" parameterType="hashmap" resultMap="customMap">
select
*
from tracker_config
where id <= ${hashmap.id}
</select>
递归完毕,
<sql id="including">
解析完毕 。
4.2 selectKey 解析
个人不太推荐
selectKey
的使用,个人感觉类似存储过程…,在一个sql里面做各种事情,例如将某个值赋值为另一个sql的结果,比如插入自增id,或者在插入完毕后,
SELECT LAST_INSERT_ID() AS xxxx
将插入主键拿到,这种需求更加推荐通过多个mapper + 业务控制、通过插件、或者修改源码的方式去写。
从代码也很容易看出来,实际上它就是构建了一个新的查询类型的
mappedStatement
,将它存到
configuration
的
KeyGenerator
中。
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
有一点需要注意的是,就是它有一个执行顺序:
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
是在本sql之前执行,还是在之后执行,其实就是简单指定一下。
它保存的map
keyGenerators
,id 为当前sql的 id,我们执行这条sql语句之前,只需要根据当前执行sql的id,就可以拿到
selectKey
语句。
public void addKeyGenerator(String id, KeyGenerator keyGenerator) {
keyGenerators.put(id, keyGenerator);
}
在生成执行语句
mappedStatement
之后,它会从 sql 中被移除,都很简单,我就不啰嗦了:
private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
for (XNode nodeToHandle : selectKeyNodes) {
nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
}
}
4.3 mappedStatement 的生成
实际上到了这里已经没什么可以讲的了,mappedStatement 就是一个存放解析对象的一个容器
MappedStatement.Builder statementBuilder = new MappedStatement.Builder(configuration, id, sqlSource, sqlCommandType)
.resource(resource)
.fetchSize(fetchSize)
.timeout(timeout)
.statementType(statementType)
.keyGenerator(keyGenerator)
.keyProperty(keyProperty)
.keyColumn(keyColumn)
.databaseId(databaseId)
.lang(lang)
.resultOrdered(resultOrdered)
.resultSets(resultSets)
.resultMaps(getStatementResultMaps(resultMap, resultType, id))
.resultSetType(resultSetType)
.flushCacheRequired(valueOrDefault(flushCache, !isSelect))
.useCache(valueOrDefault(useCache, isSelect))
.cache(currentCache);
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
它的实例化是一个比较纯粹的构造器模式,拿到各种配置的
attribute
,解析出来的
resultMap
等,加以拼装,它并不是我们理解
mybatis
的重点,我们应该更加关注参与构造的这些参数是怎么来的,由于篇幅有限,再加上博主也懒得讲解一些不常用,或者比较简单的配置。如果哪里分析的有问题,或者希望博主对哪个部分进行比较深入的分析欢迎评论~
后续会有更多 mybatis 源码讲解~~
五、小结
小小的总结一下,在 mybatis 对 xml 的解析中,常用的几个技巧:
- 递归,递归来解决嵌套问题,同时也可以避免生成的java类层级过深
- id,几乎所有的 node 节点,都会有一个自己的全限定名,配合递归来使用事半功倍
- 统一的上下文,configuration 类,基本什么东西都可以往里面塞,就像是一个哆啦A梦的四次元口袋,它本身也比较纯粹,基本不参与什么业务操作,就是存了很多解析生成的东西
- 大量的兜底配置,实际上文章里很少提到的,但是代码里很常见,即兜底配置,实际上就是一种约定优于配置的思想,许多的额外支持仅仅暴露出来作为可选项,默认是有一套自己的实现的。
附Java/C/C++/机器学习/算法与数据结构/前端/安卓/Python/程序员必读/书籍书单大全:
(点击右侧 即可打开个人博客内有干货):
技术干货小栈
=====>>
①【Java大牛带你入门到进阶之路】
<<====
=====>>
②【算法数据结构+acm大牛带你入门到进阶之路】
<<===
=====>>
③【数据库大牛带你入门到进阶之路】
<<=====
=====>>
④【Web前端大牛带你入门到进阶之路】
<<====
=====>>
⑤【机器学习和python大牛带你入门到进阶之路】
<<====
=====>>
⑥【架构师大牛带你入门到进阶之路】
<<=====
=====>>
⑦【C++大牛带你入门到进阶之路】
<<====
=====>>
⑧【ios大牛带你入门到进阶之路】
<<====
=====>>
⑨【Web安全大牛带你入门到进阶之路】
<<=====
=====>>
⑩【Linux和操作系统大牛带你入门到进阶之路】
<<=====
天下没有不劳而获的果实,望各位年轻的朋友,想学技术的朋友,在决心扎入技术道路的路上披荆斩棘,把书弄懂了,再去敲代码,把原理弄懂了,再去实践,将会带给你的人生,你的工作,你的未来一个美梦。