回顾
上一章我们已经分析了,MyBatis是如何进行初始化的了,采用建造者模式去创建出Configuration,创建者主要是XMLConfigBuilder,指挥者为SqlSessionFactoryBuilder,指挥者利用XMLConfigBuilder创建出来的Configuration来再实例化出DefaultSqlSessionFactory
主要负责解析MyBatis总配置文件的是XMLConfigBuilder,按照建造流程顺序去解析对应的标签
下面来讲MyBatis是如何解析SQL映射配置文件的,其实解析SQL映射配置文件也在MyBatis初始化的过程中,对应的流程就是去解析mapper标签的时候
XMLMapperBuilder
之前我们也提到过,对于定义了url和resource的mapper属性,会使用XMLMapperBuilder来进行完成解析,而对于class属性,则是让configuration直接存储反射生成的接口文件。。XMLMapperBuilder采用的也是建造者模式,同理采用建造者模式去按顺序解析Mapper文件的标签,此时指挥者是外部的XMLConfigBuilder,而建造者是XMLMapperBuilder,产品依然为Configuration,只不过为Configuration里面的Mapper部分
下面就来看看这个XMLMapperBuilder的作用
从代码上可以看到,XMLMapperBuilder是执行parse方法来进行解析的,我们先来看传进来的参数是什么!
//resource属性
try(InputStream inputStream = Resources.getResourceAsStream(resource)) {
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
}
//url属性
try(InputStream inputStream = Resources.getUrlAsStream(url)){
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
可以看到,InputStream都是根据给的URL或者Resource去解析生成的配置文件流(URL、Resource都是文件路径)
并且XMLMapperBuilder里面的组装的XPathParser其实就是根据SQL映射文件生成的!
源码如下
public void parse() {
//判断Configuration是不是已经加载过resource了
//也就是判断总配置文件是不是已经加载过该URL或者Resoure对应的配置文件
if (!configuration.isResourceLoaded(resource)) {
//如果没有加载过,使用XPathParser去解析mapper标签
//此时的XPahthParser里面是SQL映射文件,也就是URL或者Resource对应的XML文件
//解析mapper标签
configurationElement(parser.evalNode("/mapper"));
//将resource添加进Configuration(底层是一个HashSet)
//代表该resouce已经解析加载过了!
configuration.addLoadedResource(resource);
//绑定命名空间
bindMapperForNamespace();
}
//解析ResultMap标签
parsePendingResultMaps();
//解析CacheRefs标签
parsePendingCacheRefs();
//解析Statement标签
parsePendingStatements();
}
判断SQL映射文件是否已经解析过
我们先看一步,MyBatis是如何存储已经解析完的SQL映射文件的
MyBatis是使用一个名为loadedResources的hashSet集合来存储已经解析完的SQL映射文件的路径的!
而解析完后,也就是直接添加进该容器中
解析mapper标签
mapper标签就是整个Mapper的主体,里面包含了平常我们开发的所有SQL,与映射关系ResultMap
可以看到,对于mapper标签的解析是在XMLMapperBuilder里面进行的,对应方法为configurationElement
private void configurationElement(XNode context) {
try {
//获取namespace属性,也就是绑定的接口全限定名!!
String namespace = context.getStringAttribute("namespace");
//判空
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//建造者助理(MapperBuilderAssistance)记录当前的空间!
builderAssistant.setCurrentNamespace(namespace);
//解析cache-ref子标签
cacheRefElement(context.evalNode("cache-ref"));
//解析cache子标签
cacheElement(context.evalNode("cache"));
//解析parameterMap子标签(已经被废弃)
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
//解析resultMap子标签
resultMapElements(context.evalNodes("/mapper/resultMap"));
//解析sql子标签
sqlElement(context.evalNodes("/mapper/sql"));
//最后解析select、insert、update、delete子标签
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);
}
}
该方法其实就是解析mapper标签下面的子标签的,并且记录当前的空间
- 获取namespace
- MapperBuilderAssistance记录当前的namespace
- 解析cache-ref子标签
- 解析cache子标签
- 解析parameterMap子标签(已废弃)
- 解析resultMap子标签
- 解析sql子标签
- 最后解析select、insert、delete、update子标签
下面来看一下对于mapper标签的所有子标签是如何解析的
解析cache子标签
我们先说明一下cache标签,cache标签是给指定的命名空间开启二级缓存功能的,MyBatis默认是不会开启二级缓存,只有在相应的配置文件中添加cache节点才能给该空间开启,MyBatis的总配置文件的setting子标签里面也有全局开启缓存的cachedEnable属性,但这个仅仅只是用来控制能否使用而已,还没进行开启使用
同时还可以通过配置cache节点的相关属性,为二级缓存配置对应的特性,比如Lru、序列化,这些功能的实现都是前面我们学习Cache接口里面对应实体类的功能,本质上就是使用装饰者模式来进行增强
源码如下
private void cacheElement(XNode context) {
if (context != null) {
//获取type属性,并且如果没有,默认为PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
//通过type属性(Cache接口实现类的别名),从别名注册中心查找对应的类对象
//当自定义缓存的时候(实现Cache接口)才会使用到该别名
//同时type属性也可以是全限定类名(如果别名注册容器中没有,就会视为全限定名进行反射创建)
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取evicetion属性,默认值是Lru
//这里evicetion属性就是淘汰策略(Lru、FIFO、SoftReference、WeakReference)
String eviction = context.getStringAttribute("eviction", "LRU");
//同样从别名注册中心去取缓存的Class对象
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
//获取刷新时间间隔,默认值为null
//这个刷新时间间隔对应功能就是前面学习的ScheduledCache
//ScheduledCache默认为1小时刷新,只有当访问缓存才会触发刷新机制(判断时间间隔是否达到条件)
Long flushInterval = context.getLongAttribute("flushInterval");
//获取size属性,也就是缓存的大小,默认值为null
//其实我们看到的底层缓存,其实就是PerpetualCache,就是最底层缓存
//默认的缓存大小为1024
Integer size = context.getIntAttribute("size");
//获取readOnly属性,默认值为false,判断是否只读
//其实这里readOnly属性是用来开启序列化的!
//当readOnly为true时,readWrite为false,不开启序列化
//当readOnly为false时,readWrite为true,开启序列化
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
//获取blocking属性,默认值为false,代表是否需要进行阻塞
//阻塞就是指对应的缓存项只能有一个线程去访问(对应为BlockingCache)
//底层的实现为ConcurrentHashMap + CountDownLatch
boolean blocking = context.getBooleanAttribute("blocking", false);
//获取properties配置信息
Properties props = context.getChildrenAsProperties();
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
可以看到,对于cache节点的解析关键是下面的6个属性
-
type属性:指定的底层Cache实现类,默认为Perpetual(前面学习过的PerpetualCache,底层是一个HashMap)
-
eviction属性:指定的是淘汰策略,当缓存项达到一定数量时就要进行淘汰,对应的淘汰策略有LruCache(默认,最近最少使用)、FifoCache(队列先进先出)、WeakCache(弱引用,使用JDK的弱引用回收机制)和SoftCache(软引用,使用JDK的软引用回收机制)
-
flushInterval属性:指定的刷新缓存时间,对应的缓存为ScheduledCache,也就是设置多少时间间隔为进行刷新缓存,这里缓存刷新不是淘汰缓存项喔,而是把整个缓存清空!并且这个触发机制只有在有线程访问缓存的时候才会触发(判断当前时间离上次刷新缓存时间的时间间隔,如果达到指定值,就会进行清空,默认为1小时)
-
size属性:指定的缓存项数量阈值,当达到这个阈值时就会进行淘汰!(对应设置在采用的淘汰策略缓存上的)
-
readWrite:缓存项是否可能被修改(注意,这里是线程是否会对获取到的缓存项进行修改!MyBatis是不提倡对缓存进行修改的!!!),如果这个值为true,代表readOnly属性为false(不仅仅只读,还可以修改),那么就会对缓存项进行序列化存储,获取的时候再进行反序列化,相当于就是开启了序列化功能,线程获取到的缓存项仅仅是一个备份;如果readOnly属性为true,代表用户线程只会对其进行读,并不会改(这里只是一种承诺而已,用户线程肯定可以偷偷摸摸进行改动的),那就不会开启序列化功能,直接把缓存项返回给你
-
blocking属性:代表是否进行阻塞,这里的阻塞功能是指限制缓存项只能被一个线程去访问和添加,对应的修饰者是BlockingCache,底层是使用ConcurrentHashMap和CountDownLatch来实现的,旧版本好像是使用ConcurrentHashMap和ReentrantLock实现的
解析完属性之后,下面就是使用MapperBuilderAssistant来进行创建缓存了,根据上面提供的6中属性来进行创建
对应的源码如下
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
//可以看到这里也使用了建造者模式来建造缓存。。。
//看来MyBatis作者真的是打算让建造者模式来贯穿整个Configuration的创建过程呀。。。
//使用CacheBuilder来创建Cahche
//下面CacheBuilder调用的方法仅仅只是设置成员属性而已
//最后的build方法才是开始构建!
//并且可以看到CacheBuilder的构造方法有currentNameSpace
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
//这里要注意,这里是将淘汰策略对应的实现Cache添加进了底层的装饰器链
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//Configuration添加缓存!
configuration.addCache(cache);
//MapperBuilderAssitant装配创建好的缓存
currentCache = cache;
return cache;
}
可以看到,MyBatis使用了建造者模式来创建缓存。。对应的创造者为CacheBuilder。。
我们先看valueOrDefault这个方法
这个方法很简单,就是判断是否为空,如果为空就选为默认值。。。
下面来看一下CacheBuilder的成员属性
可以看到,里面的属性就是解析cache标签出来的属性,其中有两个例外,就是decorators与id,decorators是用来装饰链,会根据装饰链的顺序一层层的给底层Cache进行装饰,最终得到完整功能的Cache,而id,我们从前面的构造方法看出,其实就是namespace(命名空间)。
下面来看一下构建过程调用的方法
可以看到,清一色的修改对应成员属性然后返回this,而已,嗯,以后我也这样写构造者模式,谁喷我就说是MyBatis教的
这里要注意的一个方法就是addDecorator方法,这个方法是用来添加装饰器的!将装饰器添加进decorators中(底层是一个ArrayList),并且这里进来的第一个装饰器一定是淘汰策略的装饰器!
下面来看最关键的build方法
源码如下
public Cache build() {
//设置默认的实现,如果没有指定Type就使用PertualCache
//如果没有指定Type,并且如果此时装饰链为空
//代表没有指定淘汰策略,使用LruCache
//说白了,就是一切都是默认配置的话采用LruCache
setDefaultImplementations();
//通过反射,根据指定的底层Cache的Class类型来实例化Cache
Cache cache = newBaseCacheInstance(implementation, id);
//设置properties信息
setCacheProperties(cache);
//判断底层的缓存是不是PerpetualCache
if (PerpetualCache.class.equals(cache.getClass())) {
//如果是,就按照MyBatis的修饰策略来进行!
//遍历修饰器链,按顺序进行装饰!
for (Class<? extends Cache> decorator : decorators) {
//通过反射,获取此时decorator的构造器
//并且调用对应的构造方法,构造方法必须要拥有只有Cache.class类型的参数构造方法
//我们之前看过很多Cache,其都拥有一个只有Cache类型参数的构造方法
//这也是框架的体现!
//约定大于配置
//但这里就挺离谱的,此时的修饰器链上应该只有一个淘汰策略的修饰器。。
//遍历什么呢?
//个人感觉这里是为了便于扩展。。
//比如要在淘汰策略的基础上去添加其他功能
//那么仅仅需要往这个集合里面去添加对应的Cache装饰器即可!
cache = newCacheDecoratorInstance(decorator, cache);
//设置上properties属性
setCacheProperties(cache);
}
//现在仅仅完成了淘汰策略的修饰,还有其他标准的修饰器还没执行
//使用其他标准的修饰器进行修饰,比如日志、刷新、阻塞
cache = setStandardDecorators(cache);
}
//此时已经是自定义缓存了
//接下来判断是不是LoggingCache的父类,考虑对自定义缓存进行添加日志功能!
else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
//如果不是,就使用LoggingCache来进行装饰。。
//给自定义缓存添加LoggingCache的功能
cache = new LoggingCache(cache);
}
//所以我们自定义缓存的时候,是不能够使用MyBatis实现好的功能的
//比如淘汰策略、超时刷新
return cache;
}
创建一个Cache真的麻烦。。。。
-
设置默认的实现,如果没有指定底层Cache类型,则会使用PerpeTualCache(修改implementation属性),并且此时修饰器链尾空,代表没有指定淘汰策略,给修饰器链添加上LruCache,代表默认使用LruCache
-
根据implementation属性,反射创建出底层的Cache
-
判断是不是PerpetualCache作为底层Cache
-
如果是,那就代表不是自定义Cache,会先对修饰器链进行遍历,获取每一个修饰器的构造方法(这个构造方法只有一个参数就是Cache接口),使用当前的底层Cache,通过反射来创建出被修饰增强的Cache,此时创建出来的Cache又会成为下一个修饰器的底层Cache,一般来说,这里的修饰器链往往只有淘汰策略的修饰器,个人感觉这是为了扩展性才能使用一个ArrayList,当后面要在淘汰策略修饰后,再增加新的修饰器时,仅需在decorators里面进行添加即可
-
如果不是,那就代表是自定义Cache了,会考虑进行增加日志功能,前提是该底层Cache不是LoggingCache的子类,如果不是,那就需要添加日志功能,使用LoggingCache来对底层Cache进行修饰
-
当修饰器链上的修饰器都完成修饰后,接下来就是一些其他标准Cache的修饰了,比如ScheduledCache,SerializedCache、LoggingCache、SynchronizedCache和BlockingCache
-
-
返回缓存
下面就来看看是如何进行其他标准Cache修饰的
private Cache setStandardDecorators(Cache cache) {
try {
//获取已经修饰好的缓存的MetaObject对象
MetaObject metaCache = SystemMetaObject.forObject(cache);
if (size != null && metaCache.hasSetter("size")) {
//使用set方法将size属性注入
metaCache.setValue("size", size);
}
//判断clearInterval是否为null
if (clearInterval != null) {
//如果不为null,使用ScheduledCache进行修饰
cache = new ScheduledCache(cache);
//设置上相应的clearInterval属性
((ScheduledCache) cache).setClearInterval(clearInterval);
}
//判断是不是开启了readWrite
if (readWrite) {
//开启了readWrite就使用SerializedCache进行修饰
cache = new SerializedCache(cache);
}
//使用LoggingCache进行修饰
cache = new LoggingCache(cache);
//使用SynchronizedCache进行修饰
//不用开启也会使用SynchronizedCache进行修饰
//这不是就限制了该namespace的缓存只能有一个线程进行访问??
//这不是很拉跨。。
cache = new SynchronizedCache(cache);
//有了SynchronizedCache还要blockingCache干啥??
//判断是否开启blocking
if (blocking) {
//使用BlockingCache进行修饰
//都有SynchronizeCache了,你这个BlockingCache有什么用吗?
//首先我们回一下BlockingCache干了什么!
//BlockingCache关键的地方,当访问缓存获取到的值为null时
//是不会释放锁的,只有当写入该key对应的缓存才会释放锁
//所以,BlockingCache的作用是防止缓存穿透的!!!!
//即便有了Synchronized也要有BlockingCache
cache = new BlockingCache(cache);
}
//返回修饰好的缓存
return cache;
} catch (Exception e) {
throw new CacheException("Error building standard cache decorators. Cause: " + e, e);
}
}
可以看到,对于其他标准装饰器就没有花里胡巧的反射了,直接按照顺序不断进行new来进行装饰,过程如下
- 首先使用set注入size属性,创建当前缓存对应的MetaObject,进行set注入,因为size属性是属于淘汰策略上的,而淘汰策略在之前已经完成装饰了,这里延后set注入,可能也是因为,后面如果扩展,在淘汰策略后又增加了修饰器,可能会有问题
- 判断有没有设置刷新时间间隔,如果有设置,使用ScheduledCache来进行修饰,并set注入配置文件上的时间间隔
- 判断是不是开启了readWrite,如果开启了readWrite,那就使用SerializedCache进行修饰
- LoggingCache进行修饰
- SynchronizedCache进行修饰,这里真的不太懂为什么一定要SynchronizedCache修饰,那不就意味着二级缓存只能一个线程去访问了吗?
- 判断是不是开启了Blocking,如果开启了使用BlockingCache进行修饰,防止缓存穿透,当然同时也会增加额外的消耗
所以对应的修饰顺序为
这么长的篇幅,才看完一个节点的解析。。。
现在一个Cache已经被Configuration.addCache方法添加进cache集合上去
Configuration的cache集合是一个StrictMap,是一个小改动的HashMap,并且Key代表Cache的id,也就是命名空间,而Value就是cache本身
StrictMap是Configurations下的一个内部类,继承了HashMap
关键的改动在于put方法上,源码如下
@Override
@SuppressWarnings("unchecked")
public V put(String key, V value) {
//如果已经添加了
if (containsKey(key)) {
//报错,该命名空间已经存在缓存了
throw new IllegalArgumentException(name + " already contains value for " + key
+ (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value)));
}
if (key.contains(".")) {
//根据命名空间生成shortKey作为Cache在容器中的键
//其实shortKey是key根据.进行分割,保留.的后一部分
final String shortKey = getShortName(key);
if (super.get(shortKey) == null) {
super.put(shortKey, value);
} else {
super.put(shortKey, (V) new Ambiguity(shortKey));
}
}
return super.put(key, value);
}
可以看到StrictMap只是避免了HashMap自身重复添加导致覆盖的问题,同时对命名空间进行处理,具体的处理是根据.来进行划分,然后取最后的一部分
解析cache-ref子标签
上面已经看了cache标签的解析,下面再看一下cache-ref子标签的解析
通过cache标签的解析,Configurations中已经有该Cache对象了,并且对应的是对应的命名空间,同时这种关系也表明了,一个命名空间有对应的一个缓存(一个namespace对应的就是一个接口文件),一般来说,不同的命名空间不能访问对方的缓存,但如果我们偏要这么做呢?
MyBatis允许多个namespace共用一个二级缓存,使用节点来进行配置
但首先我们要知道cache-ref标签是怎么使用的,比如一个namespace开了二级缓存,并且想另外一个namespace可以用自己的二级缓存,那么另外一个namespace可以使用cache-ref标签,在namespace属性上赋值上前者的namespace
对应的方法是cacheRefElement对象
private void cacheRefElement(XNode context) {
if (context != null) {
//往configuration中添加关系
//builderAssistant保存的currentNameSpace就是自身的namespace
//获取cache-ref子标签下的namespace属性(二级缓存所属的namespace)
//Configuration是记录到cacheRefMap集合中的
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
//创建CacheRefResolver对象
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//解析Cache引用
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//如果无法解析,不会抛错
//而是记录在Configuration中的incompleteCacheRefs集合中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
- 首先将cache-ref里面的namespace属性解析出来,然后用当前自身的namespace作为key,value尾cache-ref的namespace属性,添加进Configuration中的cacheRefMap集合中去
- 使用MapperBuilderAssistant和cache-ref的namespace属性去创建CacheResolver对象
- 使用CacheResolver对象去解析Cache引用,如果解析失败会添加该CacheResolver对象进Configuration中的incompleteCacheRefs集合中,稍后再继续进行解析
大概来说就是分为两部分
- 添加进Configuration的cacheRefMap中
- 解析CacheResolver
对于第一部分
cacheRefMap是一个HashMap容器而已,并且其addCacheRef也只是简单的put进去而已
对于第二部分,解析部分才是关键所在
可以看到其本质是交由MapperBuilderAssistant去完成的,调用的是useCacheRef方法,而该方法的源码如下
public Cache useCacheRef(String namespace) {
//判空
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
//其实解析的本质只是判断想要引用的Cache是否已经创建好
try {
unresolvedCacheRef = true;
//尝试从Configuration中取缓存
Cache cache = configuration.getCache(namespace);
//如果为空
if (cache == null) {
//抛错
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
//如果Configuration中存在
//则将MapperBuilderAssistant的currentCache设置为Configuration中找出来的Cache
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
可以看到,其实所谓的解析,只是尝试从Configuration中去获取cache-ref的namespace属性对应的二级缓存,如果获取到了,那么就让MapperBuilderAssistant去装配该缓存(前面解析cache标签的时候,最终也会让MapperBuilderAssistant去装配创建好的Cache),如果没找到,则会抛出InCompleteElementException,这也是为什么上一层需要去捕捉该异常,并且将MapperBuilderAssistant与cache-ref的namespace属性放进去inCompleteCacheRefs中,因为可能存在解析Mapper的先后顺序差异,所以不能直接报错!!!
下面来看以下这个inCompleteCacheRefs集合
底层是一个LinkedList,也可以分析一下为什么使用LinkedList,个人感觉,这个inCompleteResultRefs里面是存储一些cache还没创建好的cache-ref引用的,所以最后肯定是要遍历这个集合来再次检查和处理的,并且在前面进入inCompleteResultRefs采用的是add方法,在尾部进行添加,因此对于ArrayList还是LinkedList都是一样的复杂度,但ArrayList需要去考虑扩容的情况,因此使用LinkedList会比较好
至此cache-ref子标签就解析完了
解析parameterMap子标签
die,该标签已经被废弃了,不看了
简单说一下该子标签的作用
该标签的作用其实就是指定的列名用哪种类型转换器进行转换的而已,jdbcType、javaType,不就是前面我们讲类型转换器使用到的两种Type嘛
现在MyBatis都可以自动转化了。。。根据get和set方法提供的类型,就能完成
解析resultMap子标签
对应的方法为resultMapElements方法,同样位于XMLMapperBuilder下
resultMap子标签,相信很多人都用过,因为数据库查出来的表是一个二维表,而我们是要使用Java Bean来进行封装,要进行结果集映射,将结果集中的数据给映射成一个对象出来,一般是先从结果集中取出数据,然后对应注入进对象中,再处理对象之间的关系,这样做可以实现目的,但会产生冗余代码,因此可以使用resultMap节点去定义结果集与对象属性的映射关系,自动完成映射
ResultMapping对象
在开始解析之前,我们先认识ResultMap子标签会被解析成什么对象,resultMap子标签经过MyBatis的解析,最终将会变成一系列ResultMapping对象,ResultMapping对象对应数据库中的一列与Java Bean中的一个属性之间的映射关系,说白了其实就是resultMap下的子标签,有两种,result标签和id标签
成员属性
- Configuration:configuration对象
- property:对应节点中的property属性,表示的是对应列名进行映射的属性名称
- column:对应节点中的column属性,表示的是数据库中得到的列名
- javaType:对应节点的javaType属性,映射的属性对应的Java Bean的类型(一般从resultMap标签上取出来)
- jdbcType:对应节点的jdbcType属性,column在数据库中的类型
- typeHandler:对应节点的typeHandler属性,表示的是类型处理器,会覆盖默认的类型处理器
- nestedResultMapId:对应节点的resultMap属性,就是嵌套resultMap的运用,负责将结果集中的一部分列映射成其他关联的结果对象
- notNullColumns:对应节点的notNullColumn属性,存储的是不允许为Null的列名
- columnPrefix:对应节点的columnPrefix属性,列名的前缀
- flag:子标签的类型,只有ID和CONSTRUCTOR两种
- composites:对应节点的column属性进行拆分的结果,如果该属性不为空,那么ResultMapping的column字段会为null
- resultSet:对应节点的resultSet属性
- foreigmColumn:对应节点的foreignColumn属性
- lazy:对应节点的fetchType属性,代表是否进行延迟加载
并且在ResultMapping中定义了一个内部的Builder类,ResultMapping的创建也是使用建造者模式来完成的
emmm,Builder里面的方法都是自身组装的ResultMapping对象进行修i该属性,并且返回值依然为Builder
介绍完resultMap的子标签对应的对象后,下面来看一下ResultMap标签对应的对象,MyBatis使用ResultMap对象来封装ResultMap标签的内容
ResultMap对象
属性有如下
- id:节点的id属性,唯一标识
- type:节点的type属性,对应的JavaBean类型
- resultMappings:节点的所有子标签对应的resultMapping对象,说白了就是所有的字段的映射关系,除了discriminiator节点之外的其他映射关系
- idResultMappings:带有ID标志的映射关系,比如ID节点,这里的标志就是前面我们看ResultMappings里面对应的flag
- constructorMappings:带有CONSTRUCTOR标志的映射关系,比如Constructor节点
- propertyResultMappings:不带有CONSTRUCTOR标志的映射关系
- mappedColumns:记录子标签里面的所有column属性
- mappedProperties:记录了子标签里面的所有property属性
- discriminator:鉴别器,对应的是disciminator节点
- hasNestedResultMaps:标识是否含有嵌套、引用其他的ResultMap
- hasNestedQueires:标识是否有嵌套查询,resultMap提供对特定property进行嵌套查询
- autoMapping:是否开启自动映射
与ResultMappings一样,ResultMap也有自己的一个构造者
并且对于ResultMappings的容器一般是不会改的!因此使用Collections里面的UnmodifiableList来做容器
这个Unmodifiable系列,其实仅仅只是没有去具体实现add方法而已,其实也不是没实现,而是直接抛出了异常
认识完这两个对象之后,我们就可以看ResultMap的解析原理
解析对应的方法为resultMapElement
从上面这个重载关系来看,最终调用的重载方式,相当于只传了resultMap节点和EmptyList(EmptyList也是Collections的内部类)
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType) {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
//获取resultMap标签的type属性
//如果没有type属性,就为ofType属性
//如果没有ofType属性,就为resultType属性
//如果没有resultType属性,就为javaType。。。
//getStringAttribute的方法,就是第一个参数获取不到值,就返回后面一个参数的默认值
//牛逼。。。。
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
//使用resolveClass解析type,获取到要映射的实体类
Class<?> typeClass = resolveClass(type);
//如果为空
if (typeClass == null) {
//
typeClass = inheritEnclosingType(resultMapNode, enclosingType);
}
//
Discriminator discriminator = null;
//创建resultMappings的对应容器,为一个ArrayList
//我们知道这里一开始传进来为一个EmptyList
List<ResultMapping> resultMappings = new ArrayList<>(additionalResultMappings);
//获取子标签
List<XNode> resultChildren = resultMapNode.getChildren();
//遍历子标签
for (XNode resultChild : resultChildren) {
//如果constructor节点
if ("constructor".equals(resultChild.getName())) {
//调用processConstructorElement进行解析
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
//如果是discriminator节点
//调用processDiscriminatorElement进行解析
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
//如果是其他标签
List<ResultFlag> flags = new ArrayList<>();
//对id变迁进行处理
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
//使用buildResultMappingFromContext来进行处理子标签
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
//解析完子标签之后
//获取id属性,如果没有id属性。则是getValueBasedIdentifier。。
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
//获取extends属性,extends属性用的比较小,其指定了该resultMap的继承关系
//傻眼了吧。。
//resultMap也有继承关系
String extend = resultMapNode.getStringAttribute("extends");
//获取autoMapping属性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
//将解析完的结果组合在一起,创建ResultMapResolver对象
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
//ResultMapResolver进行解决剩下的问题
//其实就是添加进去Configuration中
//参数没有Configuration???但有MapperBuilderAssistant。。
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
//如果解析失败
//添加进incompleteResultMap中,等待后续处理
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
总结一下整个过程
-
获取type属性,有很多个别名,type、ofType、resultType、javaType
-
通过type属性,使用TypeAliasRegistry去找到对应的Class对象(BaseBuilder里面有注入TypeAliasRegistry)
-
创建ArrayList容器名为resultMappings来存放解析出来的ResultMapping
-
遍历所有的子标签,根据子标签不同名称去执行对应的方法进行解析
-
constructor标签就执行processConstructorElement
-
discriminator标签就执行processDiscriminatorElement
-
对于其他标签,则是执行buildResultMappingFromContext方法,比如id标签、result标签,我们常用的标签都是经过buildResultMappingFromContext方法的,正经人谁用constructor与discriminator呀~,并且这里有个细节就是,如果遇到id标签,则会额外添加进flags集合(ArrayList)中!并且也会传进去buildResultMappingFromContext方法进行处理!
-
这3种情况,最终都会将解析结果添加进resultMappings中
-
-
获取id属性,如果id属性不存在就会去找getValueBasedIdentifier,这也是我们为什么也ResultMap很少用id
-
获取extends属性,extends属性是ResultMap的继承关系
-
获取autoMapping属性,如果没有就为Null了
-
将所有的解析结果封装成ResultMapResolver,然后再进行调用ResultMapResolver的resolve进行后续处理,其实后续处理的本质就是添加进Configuration中,但其中会进行寻找extends属性
-
如果后续处理失败,则会被添加进Configuration的incompleteResultMap容器中
从cache-ref标签和resultMap标签的解析,可以看到,最后都会封装成对应的Resolver对象来进行后续处理,为什么呢?因为两者解析都会涉及顺序问题,cache-ref引用到其他的Cache对象,可能出现该cache对象还没解析创建出来;resultMap则是拥有其继承关系。可能出现父类没有解析创建出来,因此都再经过了一层Resolver来进行后续处理,并且捕捉到对应的not find异常,将此时解析的结果存放进incomplete集合中等待后续全部解析完了之后再进行处理!
先举个栗子
<resultMap id="authorResult" type="Author">
<id property="id" column="author_id"/>
<result property="userName" column="user_name"/>
</resultMap>
其他标签的处理
先讲一下对于常用标签的处理,对应的方法为buildResultMappingFromContext,源码如下
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags) {
//判断flags
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
//如果Constructor会获取name属性
property = context.getStringAttribute("name");
} else {
//获取proerty属性!!别忘了
//从上面进来这里的话只可能为ID,或者没有
//所以一般也是取property属性
property = context.getStringAttribute("property");
}
//获取column属性
String column = context.getStringAttribute("column");
//取出javaType属性
String javaType = context.getStringAttribute("javaType");
//取出jdbcType属性
String jdbcType = context.getStringAttribute("jdbcType");
//获取select属性
String nestedSelect = context.getStringAttribute("select");
//获取resultMap属性
//如果resultMap属性为空,就会调用processNestedResultMappings
//该方法就是去解析association节点
String nestedResultMap = context.getStringAttribute("resultMap", () ->
processNestedResultMappings(context, Collections.emptyList(), resultType));
//获取notNullColumn属性
String notNullColumn = context.getStringAttribute("notNullColumn");
//获取columnPrefix属性
String columnPrefix = context.getStringAttribute("columnPrefix");
//获取typeHander属性
String typeHandler = context.getStringAttribute("typeHandler");
//获取一系列属性。。。。
String resultSet = context.getStringAttribute("resultSet");
String foreignColumn = context.getStringAttribute("foreignColumn");
boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager"));
//对javaType进行解析,与前面解析一样
//都是调用TypeAliasRegistry进行搜索,找到对应的Class
//这也是为了支持别名
Class<?> javaTypeClass = resolveClass(javaType);
//对TypeHandler同样进行解析
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
//对Jdbc类型进行解析,使用resolveJdbcType进行
//对jdbc类型的解析是通过JdbcType去进行的,就一个枚举类而已
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
///使用MapperBuilderAssistant进行进行构建出ResultMapping出来
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy);
}
可以看到,对于其他标签的处理,分为两个部分
- 获取一系列的属性值,比如JavaType、jdbcType、column、property、typeHandler等属性。。还会对javaType、typeHandler属性通过TypeAliasRegistry去找出对应的Class类型,因为可能进来的是一个别名,一定要通过别名注册中心去找!而jdbcType则是通过名字对应JdbcType这个枚举类去找到对应的JdbcType
- 比较特殊是针对association、collection、case节点的解析
- 将这些属性交由MappaerBuilderAssistant去创建出ResultMapping出来
- MapperBuilderAssistant负责了这么大的责任。。竟然还负责去创建ResultMapping
下面来看看是如何解析association节点的,当此时进来的子标签的resultMap属性为null时,则会去判断需不需要去解析association节点,对应执行的方法为processNestedResultMappings,源码如下
private String processNestedResultMappings(XNode context, List<ResultMapping> resultMappings, Class<?> enclosingType) {
//对于association、collection、case等节点进行解析
//判断进来的子标签的名字是否为association、collection、case
//并且该子标签的select属性为null
if (Arrays.asList("association", "collection", "case").contains(context.getName())
&& context.getStringAttribute("select") == null) {
//对collection标签进行校验
validateCollection(context, enclosingType);
//递归去调用resultMapElement进行解析!
//所以说,对于association、collection、case的解析过程与resultMap标签是一样的!
//并且解析的结果,里面的resultMapping也会被添加进resultMappings中!
ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType);
//返回解析好的resultMap
return resultMap.getId();
}
return null;
}
可以看到,该方法是针对association(一对一)、collection(一对多)、case等标签进行处理的
- 首先判断标签的名字是否可以对应上association、collection、case,并且select属性必须为null!
- 对collection标签进行校验
- 递归去调用resultMapElement方法进行解析,这里要注意的是传入的resultMappings参数,这会导致对于association、collection、case等标签的子标签创建出对应的resultMapping,也会被添加进该resultMappings集合中,所以此时的resultMappings集合里面存放的是resultMap标签里面的所有子标签,包括子标签的子标签创建生成的resultMapping
- 返回resultMap的id
下面再来看一下,如何对collection标签校验的,源码如下
protected void validateCollection(XNode context, Class<?> enclosingType) {
//判断是否对应上collection名字、resultMap属性为null、javaType属性也要为null
if ("collection".equals(context.getName()) && context.getStringAttribute("resultMap") == null
&& context.getStringAttribute("javaType") == null) {
//获取enclosingType对应的MetaClass对象,也就是其类信息的封装!
//这里的enclosingType其实就是resultMap标签里面的javaType
MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory());
//获取collection标签的property属性,代表集合的变量名字
String property = context.getStringAttribute("property");
//判断该集合是否存在set方法!
//如果不存在则会报错
if (!metaResultType.hasSetter(property)) {
throw new BuilderException(
"Ambiguous collection type for property '" + property + "'. You must specify 'javaType' or 'resultMap'.");
}
}
}
可以看到,对于collection方法其实就是当没有写明resultMap属性和javaType属性时,需要去去检验对应要映射的实体类是否有该集合变量的set方法!因为没有ResultMap和javaType属性,不清楚集合的类型!!!
下面就来看看是如何MapperBuilderAssistant是如何创建ResultMapping的,源码如下
public ResultMapping buildResultMapping(
Class<?> resultType,
String property,
String column,
Class<?> javaType,
JdbcType jdbcType,
String nestedSelect,
String nestedResultMap,
String notNullColumn,
String columnPrefix,
Class<? extends TypeHandler<?>> typeHandler,
List<ResultFlag> flags,
String resultSet,
String foreignColumn,
boolean lazy) {
//对javaType和typeHandler进行处理
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
List<ResultMapping> composites;
if ((nestedSelect == null || nestedSelect.isEmpty()) && (foreignColumn == null || foreignColumn.isEmpty())) {
composites = Collections.emptyList();
} else {
composites = parseCompositeColumnName(column);
}
//可以看到,底层就是使用ResultMappingBuilder来进行创建。。
//建造者模式真不错。。。。
return new ResultMapping.Builder(configuration, property, column, javaTypeClass)
.jdbcType(jdbcType)
.nestedQueryId(applyCurrentNamespace(nestedSelect, true))
.nestedResultMapId(applyCurrentNamespace(nestedResultMap, true))
.resultSet(resultSet)
.typeHandler(typeHandlerInstance)
.flags(flags == null ? new ArrayList<>() : flags)
.composites(composites)
.notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix)
.foreignColumn(foreignColumn)
.lazy(lazy)
.build();
}
可以看到也是分为两步
- 对javaType和typeHandler再次进行处理 // todo 进行如何的处理呢?
- 利用ResultMapping的Builder创建出ResultMapping出来
- 这里又用了建造者模式,产品是ResultMapping,建造者是ResultMappingBuilder,指挥者是MapperBuilderAssistant,别忘记了MapperBuilderAssistant本身也是一个建造者,那么建造者又是指挥者。。。这种设计模式称为什么呢?
constructor标签的处理
对应的方法为processConstructorElement,源码如下
private void processConstructorElement(XNode resultChild, Class<?> resultType, List<ResultMapping> resultMappings) {
//获取子标签
List<XNode> argChildren = resultChild.getChildren();
//遍历子标签,constructor的子标签其实就是arg与idArg
//
for (XNode argChild : argChildren) {
//flags中加入CONSTRUCTOR标识!
List<ResultFlag> flags = new ArrayList<>();
flags.add(ResultFlag.CONSTRUCTOR);
//如果存在子标签的名字为idArg
if ("idArg".equals(argChild.getName())) {
//添加ID标识
flags.add(ResultFlag.ID);
}
//仍然调用buildResultMappingFromContext方法。。。
//添加进resultMappings中
resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags));
}
}
从代码上可以看到,其实constructor标签的解析过程跟其他标签的解析过程是一样的,仅仅只是flags会出现不同!
而我们前面看buildResultMappingFromContext方法时,也看到了如果flags中存在Constructor,property值其实会变成name属性!!说白了对于arg、idArg这一类标签取得是name属性,name属性其实是对应构造方法里面的参数名字。。
也就是说,其实两种标签的解析步骤仅仅出现在property属性上,对于其他标签取的是property属性,而对于arg、idArg标签取的是name属性,**而constructor子标签的作用其实是使用指定的构造函数!!**也说明了flag为ID、CONSTRUCTOR和NULL究竟是区分什么的,其实是用来区分出其他标签、id标签和Constructor标签的
Resolve处理
等处理完属性、将resultMap下的子标签解析完了之后,会封装成ResultMapResolver,然后调用resolve方法进行后续处理
从代码上可以看到,是直接调用MapperBuilderAssistant的addResultMap方法的
源码如下
public ResultMap addResultMap(
String id,
Class<?> type,
String extend,
Discriminator discriminator,
List<ResultMapping> resultMappings,
Boolean autoMapping) {
//对id属性补充,完整的id属性为namespace.id
id = applyCurrentNamespace(id, false);
//对extend属性进行补充,完成的extend属性为namespace.extend
extend = applyCurrentNamespace(extend, true);
//如果extend属性不为null,代表当前ResultMap存在继承关系需要进行处理
if (extend != null) {
//判断Configuration中存不存在对应extend属性的ResultMap对象
if (!configuration.hasResultMap(extend)) {
//如果不存在,抛出InCompleteElementException
//这个异常会被上层捕捉,然后就知道extend对应的ResultMap还没完成解析创建
//会被添加到incomplete集合中去
throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'");
}
//如果存在,则从Configuration中获取出来
ResultMap resultMap = configuration.getResultMap(extend);
//创建extendedResultMappings容器,存储extend属性对应的所有ResultMappings
List<ResultMapping> extendedResultMappings = new ArrayList<>(resultMap.getResultMappings());
//对extendedResultMappings容器清除自身ResultMap的ResultMappings
//这是为了避免重复的ResultMappings,将extend的ResultMapings集合过滤掉上一层解析出来的ResultMappings
extendedResultMappings.removeAll(resultMappings);
boolean declaresConstructor = false;
//遍历前面解析出来的ResultMapping
for (ResultMapping resultMapping : resultMappings) {
//如果存在constructor标签对应的ResultMapping
if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) {
declaresConstructor = true;
break;
}
}
//如果前面解析出来的ResultMapping存在constructor类型的
if (declaresConstructor) {
//extends属性里面的ResultMappings集合要去掉所有constructor类型的!
//只能存在一个constructor类型!!
extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR));
}
//最后,将处理好的extend的ResultMappings集合与前面解析出来的ResultMapping进行合并!
resultMappings.addAll(extendedResultMappings);
}
//使用ResultMapBuilder来进行创建出ResultMap。。。
//又是一个创建者模式
ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping)
.discriminator(discriminator)
.build();
//configuration中去添加ResultMap
//底层是一个StricpMap。。
configuration.addResultMap(resultMap);
return resultMap;
}
可以看到,其实MapperBuilderAssistant的addResultMap方法大概如下
- 补充id与extend属性
- 通过extend属性获取对应的ResultMap,并且对其ResultMappings集合进行处理
- 过滤掉当前解析出来的ResultMapping
- 如果当前解析出来的ResultMappings集合中存在由constructor标签解析生成的ResultMapping,则会将extend属性中的ResultMappings集合的所有constructor类型的resultMapping删掉,仅仅保留当前解析的constructor,说白了其实就是只允许存在一种constructor!!!
- 最后将处理好的extend的ResultMappings集合添加进当前解析出来的ResultMappings集合中
- 将处理好的属性交由ResultMap的Builder来进行创建ResultMap,MapperBuilderAssistant这里又称为指挥者,而创建者是Builder
- configuration中添加创建好的ResultMap,底层存储ResultMap是一个StrictMap
至此Resolve处理就完事了!
至此resultMap标签解析完了
解析SQL子标签
SQL子标签我们同样也用的比较少
在映射配置文件中,可以使用SQL节点去定义可重用的SQL语句片段,当需要使用SQL节点里面定义的SQL语句片段时,只需要使用include标签引入相应的片段即可,而include节点,我们是在后面的select、delete等节点中使用的
对应解析sql子标签的方法为sqlElement
private void sqlElement(List<XNode> list) {
//交由重载的sqlElement方法去解析
//如果configuration中存在databaseId,就传进去
if (configuration.getDatabaseId() != null) {
sqlElement(list, configuration.getDatabaseId());
}
sqlElement(list, null);
}
private void sqlElement(List<XNode> list, String requiredDatabaseId) {
//遍历sql节点的所有子标签
for (XNode context : list) {
//获取databaseId属性
String databaseId = context.getStringAttribute("databaseId");
//获取id属性
String id = context.getStringAttribute("id");
//补充id属性,完整的id属性为namespace.id
id = builderAssistant.applyCurrentNamespace(id, false);
//判断sql的databaseid与configuration中的databaseId是否一致
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
//往configuration中的sqlFragments集合中添加键值对
//key为sql子标签的id,context为子标签
//sqlFragments为XMLMapperBuilder的成员属性,
//但是其实指向与Configuration中的sqlFragments
sqlFragments.put(id, context);
}
}
}
可以看到,其实就是遍历sql节点下的子标签,然后与id形成键值对添加进sqlFragments中而已,并且id前缀为当前的namespace,因此对于不同的namespace其实支持相同的id!!!
先看完这几种标签的解析,后续还有更重要的select、delete、update、insert等sql!