XMLMapperBuilder
介绍
XMLMapperBuilder类继承自BaseBuilder类,该类也使用了建造者设计模式。XMLMapperBuilder类的主要作用是解析xxxMapper.xml配置文件中用户的配置,如下图所示。
mybatis-3-mapper.dtd文件中对于mapper节点的定义如下:
XMLMapperBuilder类的核心功能就是解析<mapper></mapper>
标签中配置的内容。
字段
XMLMapperBuilder类中定义的字段如下
//xpath解析器,用于解析用于配置的xml
private final XPathParser parser;
//Mapper建造者助手,一个小工具
private final MapperBuilderAssistant builderAssistant;
//sql片段集合,key为sql片段的namespace+id,value为对应的XNode节点
private final Map<String, XNode> sqlFragments;
//xml配置的资源路径
private final String resource;
构造函数
除了两个已经过时的方法外,所有的构造函数都通过重载调用了最后一个构造函数
@Deprecated
public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
this(reader, configuration, resource, sqlFragments);
this.builderAssistant.setCurrentNamespace(namespace);
}
@Deprecated
public XMLMapperBuilder(Reader reader, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(reader, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments, String namespace) {
this(inputStream, configuration, resource, sqlFragments);
//给builderAssistant设置命名空间
this.builderAssistant.setCurrentNamespace(namespace);
}
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
//保存全局的配置,并创建一个BuilderAssistant实例,便于后续使用
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource, Map<String, XNode> sqlFragments) {
super(configuration);
//构造一个builderAssistant实例,方便后面解析
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
核心方法
XMLMapperBuilder类的核心方法为parse()方法,通过调用该方法,触发对mapper.xml文件中内容的解析。
parse()方法的主要逻辑是解析mapper节点中的配置,并做一些后续的收尾工作。parse()方法的定义如下:
/**
* XMLMapperBuilder类的核心方法,是解析mapper.xml文件配置的入口
*/
public void parse() {
//防止重复加载
if (!configuration.isResourceLoaded(resource)) {
//解析mapper配置
configurationElement(parser.evalNode("/mapper"));
//将读取过的配置文件的地址保存在configuration对象的loadedResources字段中,
//该字段用于防止资源被重复加载
configuration.addLoadedResource(resource);
//为读取的mapper配置信息绑定命名空间
bindMapperForNamespace();
}
//解析待定的<resultMap>节点
parsePendingResultMaps();
//解析待定的<cache-ref>节点
parsePendingCacheRefs();
//解析待定的sql片段节点
parsePendingStatements();
}
解析mapper配置
该方法的作用是根据dtd文件中的定义,逐一解析<mapper></mapper>
下的所有配置,方法实现如下:
private void configurationElement(XNode context) {
try {
//获取配置文件中配置的名称空间
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.isEmpty()) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
//设置BuilderAssistant实例当前的命名空间为mapper.xml文件中配置的命名空间
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);
}
}
解析缓存引用配置
对于< cache-ref />的作用,可参考官网文档的介绍:
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。如果想要在多个命名空间中共享相同的缓存配置和实例,则可以使用 cache-ref 元素来引用另一个缓存
cacheRefElement方法的主要逻辑就是构建一个缓存引用解析器对象CacheRefResolver,并通过CacheRefResolver对象来解析用户配置的缓存
private void cacheRefElement(XNode context) {
if (context != null) {
//在全局配置对象的cacheRefMap字段中维护缓存引用的关系,
//cacheRefMap中的映射关系为,当前命名空间--->配置中声明的名称空间
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
//构造CacheRefResolver对象
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
//解析缓存引用,实际的解析工作是委派给builderAssistant对象来完成
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
//如果在解析的过程中抛出了IncompleteElementException异常,则将当前的配置解析器对象添加到configuration对象的待完成缓存引用的列表中
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
CacheRefResolver类
根据resolveCacheRef()方法的定义可以发现,mybatis对于缓存引用的解析是委派给了MapperBuilderAssistant类的实例去完成的。
public class CacheRefResolver {
//构造小助手
private final MapperBuilderAssistant assistant;
//引用的命名空间
private final String cacheRefNamespace;
public CacheRefResolver(MapperBuilderAssistant assistant, String cacheRefNamespace) {
this.assistant = assistant;
this.cacheRefNamespace = cacheRefNamespace;
}
public Cache resolveCacheRef() {
return assistant.useCacheRef(cacheRefNamespace);
}
}
MapperBuilderAssistant#useCacheRef方法的定义如下:
/**
*
* @param namespace 配置文件中引用的名称空间
* @return
*/
public Cache useCacheRef(String namespace) {
if (namespace == null) {
throw new BuilderException("cache-ref element requires a namespace attribute.");
}
try {
unresolvedCacheRef = true;
//在全局配置对象中查找该 namespace 对应的缓存对象
Cache cache = configuration.getCache(namespace);
//如果在configuration对象中没有找到对应的缓存实例,则抛出异常
//并将当前的解析任务放进incompleteCacheRefs集合中,后续会调用
//XmlMapperBuilder#parsePendingCacheRefs()方法对其进行再次解析
if (cache == null) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.");
}
currentCache = cache;
unresolvedCacheRef = false;
return cache;
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e);
}
}
解析缓存配置
二级缓存默认配置时的行为
mybatis二级缓存默认配置的作用如下,他的作用域是全局的:
- 映射语句文件中的所有 select 语句的结果将会被缓存。
- 映射语句文件中的所有 insert、update 和 delete 语句会刷新缓存。
- 缓存会使用最近最少使用算法(LRU, Least Recently Used)算法来清除不需要的缓存。
- 缓存不会定时进行刷新(也就是说,没有刷新间隔)。
- 缓存会保存列表或对象(无论查询方法返回哪种)的 1024 个引用。
- 缓存会被视为读/写缓存,这意味着获取到的对象并不是共享的,可以安全地被调用者修改,而不干扰其他调用者或线程所做的潜在修改。
cacheElement的代码如下,其中:
- type配置的是缓存的类型,默认为PERPETUAL,也就是使用HashMap作为缓存的容器。
- eviction配置的是缓存的清除策略,可用的清除策略有LRU、FIFO、SOFT和WEAK,其中LRU为默认策略。
- flushInterval获取缓存的清除间隔,可以配置为任意正整数,单位为毫秒,默认为0。
- size配置的是最大缓存的引用个数,可以是任意正整数,默认为缓存1024个引用。
- readOnly属性可以被设置为 true 或 false。只读的缓存会给所有调用者返回缓存对象的相同实例。 因此这些对象不能被修改。这就提供了可观的性能提升。而可读写的缓存会(通过序列化)返回缓存对象的拷贝。 速度上会慢一些,但是更安全,因此默认值是 false。
- blocking表示当在缓存中获取不到数据时,是否会阻塞后续的请求,默认为false。
//解析<cache/>配置,最后委派给MapperBuilderAssistant的实例去完成缓存对象的创建任务
private void cacheElement(XNode context) {
if (context != null) {
//获取type属性的配置,如果未配置,则默认值为PERPETUAL
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
//获取eviction属性的配置,如果未配置,则默认值为LRU
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
//获取eviction属性的配置
Long flushInterval = context.getLongAttribute("flushInterval");
//获取size属性的配置
Integer size = context.getIntAttribute("size");
//获取readOnly属性的配置
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
//获取blocking属性的配置
boolean blocking = context.getBooleanAttribute("blocking", false);
//获取<cache/>标签中配置的property属性
Properties props = context.getChildrenAsProperties();
//根据用户配置创建一个缓存对象,并将其添加到configuration的caches属性中
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
MapperBuilderAssistant#useNewCache方法定义如下:
该方法会创建一个缓存建造者对象(建造者设计模式),并通过其创建一个缓存对象
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
Cache cache = new CacheBuilder(currentNamespace)
.implementation(valueOrDefault(typeClass, PerpetualCache.class))
.addDecorator(valueOrDefault(evictionClass, LruCache.class))
.clearInterval(flushInterval)
.size(size)
.readWrite(readWrite)
.blocking(blocking)
.properties(props)
.build();
//将缓存对象保存在Configuration配置对象中
configuration.addCache(cache);
currentCache = cache;
return cache;
}
绑定mapper接口
解析mapper.xml的工作完成后需要为每一个配置文件都绑定一个Mapper接口,并注册到MapperRegistry中,配置文件都绑定Mapper接口的工作由XMLMapperBuilder#bindMapperForNamespace方法完成,其定义如下:
/**
* 根据namespace绑定mapper.xml与mapper接口的关系
*/
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
//解析namespace所对应的类型
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
if (boundType != null && !configuration.hasMapper(boundType)) {
// Spring may not know the real resource name so we set a flag
// to prevent loading again this resource from the mapper interface
// look at MapperAnnotationBuilder#loadXmlResource
//追加namespace前缀并添加到configuration对象loadedResources属性中
configuration.addLoadedResource("namespace:" + namespace);
//调用mapperRegistry的方法注册mapper接口
configuration.addMapper(boundType);
}
}
}
处理Pending集合
mybatis在处理xml配置文件时,是按照从上到下顺序进行解析的,但是在解析一个节点的时候,该节点可能引用后面定义的还未解析的节点,遇到这种情况时,mybatis会抛出对应的IncompleteExementException异常。根据抛出异常的节点不同,mybatis会创建不同的解析对象,并添加到confiuration对象中暂存,便于后续再次处理。
parsePendingResultMaps、parsePendingCacheRefs和parsePendingStatements方法便是用于处理这种情况
parsePendingResultMaps
private void parsePendingResultMaps() {
//获取resultMap对应的集合
Collection<ResultMapResolver> incompleteResultMaps = configuration.getIncompleteResultMaps();
synchronized (incompleteResultMaps) {//同步,防止重复解析
//迭代器迭代其中的元素,逐一进行解析
Iterator<ResultMapResolver> iter = incompleteResultMaps.iterator();
while (iter.hasNext()) {
try {
iter.next().resolve();
iter.remove();
} catch (IncompleteElementException e) {
// ResultMap is still missing a resource...
}
}
}
}
parsePendingCacheRefs和parsePendingStatements方法的逻辑与parsePendingResultMaps类似,不做分析