一、<mappers>元素的结构
在Mybatis-config.xml中,<mappers>元素的配置方式有以下几种:
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
<mapper resource="org/mybatis/builder/BlogMapper.xml"/>
<mapper resource="org/mybatis/builder/PostMapper.xml"/>
</mappers>
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
<mapper url="file:///var/mappers/BlogMapper.xml"/>
<mapper url="file:///var/mappers/PostMapper.xml"/>
</mappers>
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
<mapper class="org.mybatis.builder.BlogMapper"/>
<mapper class="org.mybatis.builder.PostMapper"/>
</mappers>
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
从定义文档结构的mybatis-3-config.dtd文件中,可以看出<mappers>元素可以允许的结构:
- <mappers>元素下可以有<mapper>、<package>子元素
- <mapper>子元素可以有resource、url、class三个属性中的一个,且只能有一个
- <package>子元素必须有属性package。
<!ELEMENT configuration (properties?, settings?, typeAliases?, typeHandlers?, objectFactory?, objectWrapperFactory?, reflectorFactory?, plugins?, environments?, databaseIdProvider?, mappers?)>
<!ELEMENT mappers (mapper*,package*)>
<!ELEMENT mapper EMPTY>
<!ATTLIST mapper
resource CDATA #IMPLIED
url CDATA #IMPLIED
class CDATA #IMPLIED
>
<!ELEMENT package EMPTY>
<!ATTLIST package
name CDATA #REQUIRED
>
通过上面内容我们可以知道,在<mappers>元素中,有两类子元素<mapper>和<package>,其中<mapper>子元素又有三种属性可以加载对应的配置文件,分别是:resource、url和class。在这几种配置方法中,底层最终都是通过两种方式实现,一种是面向接口编程的方式:通过MapperRegisty的addMapper()方法实现了映射关系的解析和注册,另外一种是解析普通映射配置文件的方式,后面会分别讲解。
二、<mappers>元素解析入口
<mappers>元素的解析 入口如下所示:
/**
* 解析配置文件中的mappers元素
* @param parent
* @throws Exception
*/
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {//解析<package>元素
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {//解析<mapper>元素
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url == null && mapperClass != null) {
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
} else {
throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one.");
}
}
}
}
}
针对解析<mappers>子元素的方法不同,我下面分为三类来分析:
- 解析<package>元素
该类型的解析,首先是通过属性name获取表示的包名,然后通过反射机制,加载包下所有符合条件的类,然后在通过MapperRegisty的addMapper()方法实现了映射关系的解析和注册。 - 解析属性为resource或url的<mapper>元素
使用<mapper>子元素且使用的属性是resource或url时,他们的解析方式基本上是一样。首先通过资源路径加载资源,然后构建XMLMapperBuilder实例对象,再通过XMLMapperBuilder实例对象的parse()方法来完成解析和注册。 - 解析属性为class的<mapper>元素
使用<mapper>子元素且使用的属性是class时,这种方法和解析<package>元素方法类似,区划就是:一个解析包下的多个类,一个只解析一个类而已,所以仅前置处理不一样。直接通过configuration.addMapper()方法进行解析,而该方法其实就是直接调用了MapperRegisty的addMapper()方法实现了映射关系的解析和注册。
三、解析属性为class的<mapper>元素
1、解析过程详解
这种方法是最基础的,解析<package>元素的解析过程都是通过多步解析之后,然后底层调用了该方法。我们首先来分析它的解析过程。
主要流程:
- XMLConfigBuilder.mapperElement()方法
该方法直接执行后续addMapper()方法。 - configuration.addMapper()方法
执行mapperRegistry.addMapper()方法。 - mapperRegistry.addMapper()方法
1>、为MapperRegistry.knownMappers属性赋值,之后hasMapper()方法判断将返回true
2>、执行MapperAnnotationBuilder.parse()方法 - MapperAnnotationBuilder.parse()方法
判断configuration.isResourceLoaded(),返回false时,进入loadXmlResource()方法执行。 - MapperAnnotationBuilder.loadXmlResource()方法
判断configuration.isResourceLoaded(),如果返回false,则执行XMLMapperBuilder.parse()方法。 - XMLMapperBuilder.parse()方法
1>、首先判断configuration.isResourceLoaded()方法,如果返回false,继续执行。
2>、通过configurationElement()方法解析Mapper配置文件中的元素。
3>、为configration的loadedResources属性赋值,之后configuration.isResourceLoaded()方法将返回true。
4>、进入bindMapperForNamespace()方法 - XMLMapperBuilder.bindMapperForNamespace()方法
判断configuration.hasMapper()方法,如果返回false,即表示没有解析对应类的映射配置文件,则执行configuration.addMapper()方法,即又回到了第二步进行执行,否则则不执行任何有效逻辑代码。然后方法执行结束,回到了XMLMapperBuilder.parse()方法中,执行后续代码,然后又回到了loadXmlResource()方法,方法执行完后,又回到了MapperAnnotationBuilder.parse()方法中,继续执行后续的方法。在该流程中,configuration.hasMapper()方法会返回true,因为在第3步,已经通过mapperRegistry.addMapper()方法添加了对应的解析属性,所以该方法在该流程中其实是没有做任何处理的。 - 从第8步后,又回到了MapperAnnotationBuilder.parse()方法中,后续开始执行configuration.addLoadedResource()方法,即为configration的loadedResources属性赋值,之后configuration.isResourceLoaded()方法将返回true。然后设置MapperBuilderAssistant的currentNamespace值,即设置当前映射文件的命名空间。
- 执行parseCache()、parseCacheRef()方法
这两个方法主要是用来解析对应类上@CacheNamespaceRef、@CacheNamespace 注解,需要注意的是,解析这两个注解的结果会覆盖上述第六步XMLMapperBuilder.parse()方法中configurationElement()方法解析<cache-ref>、<cache>两个元素的结果,即注解配置优先XML配置。 - 循环执行parseStatement()方法
该方法主要是解析对应类上的sqlAnnotationType(@Select、@Insert、@Update、@Delete)、sqlProviderAnnotationTypes(@SelectProvider、@InsertProvider、@UpdateProvider、@DeleteProvider)两种类型的注解,解析结果会覆盖中上述第六步XMLMapperBuilder.parse()方法中configurationElement()方法解析select|insert|update|delete等元素节点的结果,和上一步一样,即注解配置优先XML配置。 - 当第10步的方法执行完后,就会回到了mapperRegistry.addMapper()方法中,执行后续方法后,然后又回到configuration.addMapper(),最后回到了XMLConfigBuilder.mapperElement()方法中,然后就完成了一个mybatis配置文件中一个mapper元素的解析过程。
接下来,哦们详细分析每一步执行的操作逻辑,首先当resource和url都为空,且class不为空时,进入该类型的解析过程,首先获取mapperClass对应的Class实例,然后调用configuration.addMapper()方法,代码如下:
//XMLConfigBuilder.java文件中的mapperElement()方法
Class<?> mapperInterface = Resources.classForName(mapperClass);
configuration.addMapper(mapperInterface);
在configuration.addMapper()方法中,调用了mapperRegistry.addMapper()方法了,上面已经提到,该方法其实就是解析和注册Mapper映射信息的核心方法。
//Configuration.java
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
下面就详细分析mapperRegistry.addMapper()方法,该方法的逻辑如下
- 首先判断当前type是接口类型,否则不做任何处理。
- 判断是否已经解析并注册
通过MapperRegistry的hasMapper()方法验证是否已经注册,如果已经注册,直接抛出BindingException异常。其中hasMapper()方法,实际上就是通过MapperRegistry的knownMappers字段是否包含对应type的key键来判断的,因为每次解析一个type类时,都会添加到knownMappers字段中。代码如下:public <T> boolean hasMapper(Class<T> type) { return knownMappers.containsKey(type); }
- 定义loadCompleted=false,表示开始解析type对应的映射文件,且表示当前还未解析完成。
- 把type及其对应的代理工厂类添加到knownMappers字段中
- MapperAnnotationBuilder.parse()方法解析配置文件
该过程比较复杂,下面单独分析。 - 修改注册状态,loadCompleted = true;表示完成了该文件的解析和注册工作。
- 执行finally代码块
在finally代码块中,主要是处理因异常未完成文件解析和注册工作,当时已经添加到knownMappers属性中的type,即移除knownMappers中的type即可。
addMapper()方法的完整代码如下:
//MapperRegistry.java
/**
* 在MyBatis初始化过程中会读取映射配置文件以及Mapper接口中的注解信息,
* 并调用MapperRegisty.addMapper()方法填充MapperRegistry.knownMappers集合,
* 该集合的key是Mapper接口对应的Class对象, value为MapperProxyFactory工厂对象。
*
* @param type
*/
public <T> void addMapper(Class<T> type) {
if (type.isInterface()) {//验证要添加的映射器的类型是否是接口
if (hasMapper(type)) {//验证注册器集合中是否已存在该注册器(即重复注册验证),如果已存在则抛出绑定异常
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
//定义一个boolean值,默认为false,标识是否加载完成
boolean loadCompleted = false;
try {
//将Mapper接口对应的Class对象和MapperProxyFactory对象添加到knownMappers集合
knownMappers.put(type, new MapperProxyFactory<T>(type));
// It's important that the type is added before the parser is run
// otherwise the binding may automatically be attempted by the
// mapper parser. If the type is already known, it won't try.
//对使用注解方式的实现进行注册(一般不使用)
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
//设置loadCompleted的值为true,表示注册完成
loadCompleted = true;
} finally {
if (!loadCompleted) {//对注册失败的类型进行清除
knownMappers.remove(type);
}
}
}
2、addMapper()方法中MapperAnnotationBuilder.parse()方法解析配置文件的逻辑
这里主要分析上面提到的解析配置文件的逻辑,这一步是整个addMapper()方法最核心的代码,代码如下:
//MapperRegistry.java文件addMapper()方法的代码片段
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
parser.parse();
首先构建了MapperAnnotationBuilder 实例对象,然后通过parse()方法完成解析和注册逻辑。在这里有一点儿不太好理解的是:MapperAnnotationBuilder类的命名方法,好像应该是处理注解方式的映射配置,其实不是这样子的,这里是通用的处理方式,可以处理xml方法和注解方式两种配置,只不过如果同时存在,注解配置会覆盖掉XML方式的配置。
构建实例的代码如下:
构建实例的过程主要完成了一下几件事:
- 根据type类型,预测了对应的类文件路径。即把全限定类型中“.”换成“/”,然后添加“.java”后缀生成的预测类文件路径。
- 然后根据该resource构建MapperBuilderAssistant实例对象,该对象主要是辅助解析映射文件中各类元素,这里不再展开分析。
- 为sqlAnnotationTypes和sqlProviderAnnotationTypes等赋值
//MapperAnnotationBuilder.java
public MapperAnnotationBuilder(Configuration configuration, Class<?> type) {
String resource = type.getName().replace('.', '/') + ".java (best guess)";
this.assistant = new MapperBuilderAssistant(configuration, resource);
this.configuration = configuration;
this.type = type;
sqlAnnotationTypes.add(Select.class);
sqlAnnotationTypes.add(Insert.class);
sqlAnnotationTypes.add(Update.class);
sqlAnnotationTypes.add(Delete.class);
sqlProviderAnnotationTypes.add(SelectProvider.class);
sqlProviderAnnotationTypes.add(InsertProvider.class);
sqlProviderAnnotationTypes.add(UpdateProvider.class);
sqlProviderAnnotationTypes.add(DeleteProvider.class);
}
parse()方法
parse()方法是完成解析和注册的核心方法。代码如下:
//MapperAnnotationBuilder.java
public void parse() {
String resource = type.toString();
if (!configuration.isResourceLoaded(resource)) {
loadXmlResource();
configuration.addLoadedResource(resource);
assistant.setCurrentNamespace(type.getName());
parseCache();
parseCacheRef();
Method[] methods = type.getMethods();
for (Method method : methods) {
try {
// issue #237
if (!method.isBridge()) {
parseStatement(method);
}
} catch (IncompleteElementException e) {
configuration.addIncompleteMethod(new MethodResolver(this, method));
}
}
}
parsePendingMethods();
}
通过分析上面代码,可以知道parse()方法逻辑过程如下:
-
首先判断该映射文件是否被加载
主要是configuration.isResourceLoaded()方法进行判断,其实就是在configration全局变量中维护了一个Set<String>类型的loadedResources变量,isResourceLoaded()方法就是判断该变量是否已经包括了对应的type。向loadedResources变量添加数据的方式,就是通过configuration.addLoadedResource()方法完成,即解析过一个XML映射文件,就把对应的信息记录到loadedResources变量中。 -
loadXmlResource()方法加载对应的映射文件
该方法的逻辑:首先根据configuration.isResourceLoaded()方法判断是否已经加载对应的资源,如果没有就根据type的全限定类型推测对应的映射文件位置,然后加载对应的文件资源生成对应的文件流对象inputStream,最后通过XMLMapperBuilder的parse()方法完成XML映射文件的真正的解析过程。XMLMapperBuilder的parse()方法后续单独分析。//MapperAnnotationBuilder.java private void loadXmlResource() { // Spring may not know the real resource name so we check a flag // to prevent loading again a resource twice // this flag is set at XMLMapperBuilder#bindMapperForNamespace if (!configuration.isResourceLoaded("namespace:" + type.getName())) { String xmlResource = type.getName().replace('.', '/') + ".xml"; InputStream inputStream = null; try { inputStream = Resources.getResourceAsStream(type.getClassLoader(), xmlResource); } catch (IOException e) { // ignore, resource is not required } if (inputStream != null) { XMLMapperBuilder xmlParser = new XMLMapperBuilder(inputStream, assistant.getConfiguration(), xmlResource, configuration.getSqlFragments(), type.getName()); xmlParser.parse(); } } }
-
添加type到loadedResources变量,即第一步提到的添加记录操作。设置当前映射文件的命名空间,即为对应的MapperBuilderAssistant实例的currentNamespace属性赋值。
-
解析CacheNamespace注解
解析CacheNamespace注解,该注解对应的对象与XML配置文件中的cache元素作用一样。如果在loadXmlResource()方法中,通过调用XMLMapperBuilder.parse()已经解析了cache元素,这个时候如果也存在对应注解的话,注解的实例对象就会覆盖XML对应,即注解配置优先于XML配置。 解析CacheNamespace注解的代码如下,最后使用MapperBuilderAssistant实例的辅助方法useNewCache()创建对应的Cache实例,并注册到configuration全局变量中。MapperBuilderAssistant的useNewCache()方法比较简单,这里不再贴出代码。//MapperAnnotationBuilder.java private void parseCache() { CacheNamespace cacheDomain = type.getAnnotation(CacheNamespace.class); if (cacheDomain != null) { Integer size = cacheDomain.size() == 0 ? null : cacheDomain.size(); Long flushInterval = cacheDomain.flushInterval() == 0 ? null : cacheDomain.flushInterval(); Properties props = convertToProperties(cacheDomain.properties()); assistant.useNewCache(cacheDomain.implementation(), cacheDomain.eviction(), flushInterval, size, cacheDomain.readWrite(), cacheDomain.blocking(), props); } }
-
解析CacheNamespaceRef注解
该方法和上述第4步中解析CacheNamespace注解的作用类似,主要用来解析CacheNamespaceRef注解,且同样是注解配置优先于XML配置。解析CacheNamespaceRef注解的代码如下,最后使用MapperBuilderAssistant实例的辅助方法useCacheRef()创建对应的Cache实例,并更新configuration全局变量中对应的caches数据。MapperBuilderAssistant的useCacheRef()方法比较简单,这里不再贴出代码。//MapperAnnotationBuilder.java private void parseCacheRef() { CacheNamespaceRef cacheDomainRef = type.getAnnotation(CacheNamespaceRef.class); if (cacheDomainRef != null) { Class<?> refType = cacheDomainRef.value(); String refName = cacheDomainRef.name(); if (refType == void.class && refName.isEmpty()) { throw new BuilderException("Should be specified either value() or name() attribute in the @CacheNamespaceRef"); } if (refType != void.class && !refName.isEmpty()) { throw new BuilderException("Cannot use both value() and name() attribute in the @CacheNamespaceRef"); } String namespace = (refType != void.class) ? refType.getName() : refName; assistant.useCacheRef(namespace); } }
-
parseStatement()方法解析对应update、insert、delete、select等注解。
该方法和上述第4、5步中解析CacheNamespace、CacheNamespaceRef注解的作用类似,parseStatement方法主要用来解析方法上对应的update、insert、delete、select等注解,且同样是注解配置优先于XML配置。解析注解的代码如下。该方法在循环处理每个注解时,出现解析异常的方法会记录到configration的全局变量incompleteMethods属性中,循环结束后,会通过parsePendingMethods()方法处理掉异常的数据记录。
void parseStatement(Method method) {
Class<?> parameterTypeClass = getParameterType(method);
LanguageDriver languageDriver = getLanguageDriver(method);
SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
if (sqlSource != null) {
Options options = method.getAnnotation(Options.class);
final String mappedStatementId = type.getName() + "." + method.getName();
Integer fetchSize = null;
Integer timeout = null;
StatementType statementType = StatementType.PREPARED;
ResultSetType resultSetType = ResultSetType.FORWARD_ONLY;
SqlCommandType sqlCommandType = getSqlCommandType(method);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
boolean flushCache = !isSelect;
boolean useCache = isSelect;
KeyGenerator keyGenerator;
String keyProperty = "id";
String keyColumn = null;
if (SqlCommandType.INSERT.equals(sqlCommandType) || SqlCommandType.UPDATE.equals(sqlCommandType)) {
// first check for SelectKey annotation - that overrides everything else
SelectKey selectKey = method.getAnnotation(SelectKey.class);
if (selectKey != null) {
keyGenerator = handleSelectKeyAnnotation(selectKey, mappedStatementId, getParameterType(method), languageDriver);
keyProperty = selectKey.keyProperty();
} else if (options == null) {
keyGenerator = configuration.isUseGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
} else {
keyGenerator = options.useGeneratedKeys() ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
keyProperty = options.keyProperty();
keyColumn = options.keyColumn();
}
} else {
keyGenerator = NoKeyGenerator.INSTANCE;
}
if (options != null) {
if (FlushCachePolicy.TRUE.equals(options.flushCache())) {
flushCache = true;
} else if (FlushCachePolicy.FALSE.equals(options.flushCache())) {
flushCache = false;
}
useCache = options.useCache();
fetchSize = options.fetchSize() > -1 || options.fetchSize() == Integer.MIN_VALUE ? options.fetchSize() : null; //issue #348
timeout = options.timeout() > -1 ? options.timeout() : null;
statementType = options.statementType();
resultSetType = options.resultSetType();
}
String resultMapId = null;
ResultMap resultMapAnnotation = method.getAnnotation(ResultMap.class);
if (resultMapAnnotation != null) {
String[] resultMaps = resultMapAnnotation.value();
StringBuilder sb = new StringBuilder();
for (String resultMap : resultMaps) {
if (sb.length() > 0) {
sb.append(",");
}
sb.append(resultMap);
}
resultMapId = sb.toString();
} else if (isSelect) {
resultMapId = parseResultMap(method);
}
assistant.addMappedStatement(
mappedStatementId,
sqlSource,
statementType,
sqlCommandType,
fetchSize,
timeout,
// ParameterMapID
null,
parameterTypeClass,
resultMapId,
getReturnType(method),
resultSetType,
flushCache,
useCache,
// TODO gcode issue #577
false,
keyGenerator,
keyProperty,
keyColumn,
// DatabaseID
null,
languageDriver,
// ResultSets
options != null ? nullOrEmpty(options.resultSets()) : null);
}
}
-
parsePendingMethods()方法处理解析异常的SQL节点。
处理过程非常简单,即把全局变量configration的incompleteMethods属性中的记录,依次删除。//MapperAnnotationBuilder.java private void parsePendingMethods() { Collection<MethodResolver> incompleteMethods = configuration.getIncompleteMethods(); synchronized (incompleteMethods) { Iterator<MethodResolver> iter = incompleteMethods.iterator(); while (iter.hasNext()) { try { iter.next().resolve(); iter.remove(); } catch (IncompleteElementException e) { // This method is still missing a resource } } } }
3、XMLMapperBuilder.parse()方法
在上述介绍loadXmlResource()方法的时候,提到真正加载XML配置文件的方法,其实就是在XMLMapperBuilder.parse()方法中进行的。这里就专门来分析parse()方法的用法。首先贴出代码,如下:
//XMLMapperBuilder.java
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
通过上面代码我们可以看到,该方法的执行的逻辑如下:
- 首先判断映射文件是否已经加载,如果没有加载,就执行后续方法进行加载,否则该方法就执行完成(即不执行任何有效代码)。
- 如果没有加载,就通过configurationElement()方法加载Mapper配置文件中对应的各个元素
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);
}
}
- 通过configuration.addLoadedResource();添加已加载的资源路径,用于后续判断对应Mapper映射文件是否已经加载。
- bindMapperForNamespace()方法
用来判断是否已经加载了对应类的映射配置信息,如果没有加载(当仅使用注解进行配置映射信息时发生)就重新开始执行configuration.addMapper()进行加载。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!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
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
四、解析<package>元素
该类型的解析,首先是通过解析<package>元素的属性name获取表示的包名,然后通过反射机制,加载包下所有符合条件的类,然后在通过MapperRegisty的addMapper()方法实现了映射关系的解析和注册。本质上就是上述解析属性为class的元素的多次执行。具体过程如下所示:
首先,在解析<mappers>的子元素过程中,当子元素是<package>时,进入如下代码:
//XMLConfigBuilder.java文件中的mapperElement()方法
if ("package".equals(child.getName())) {//解析<package>元素
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
}
根据上述代码,可以知道,在该方法中又调用了configuration.addMappers()方法,在configuration.addMappers()方法中,又调用了mapperRegistry.addMappers()方法,具体代码如下:
//Configuration.java
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
//MapperRegistry.java
/**
* 用于仅指定包名的情况下,扫描包下的每个映射器进行注册
* @since 3.2.2
*/
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
在MapperRegistry的addMappers()方法中,通过重载方法addMappers(String packageName, Class<?> superType),把对应包下面的所有符合要求的类通过反射机制全部解析出来,然后再调用MapperRegistry的addMapper()方法实现具体的解析过程,该过程在上一节《解析属性为class的元素》中,已经分析,这里不再重复。
//MapperRegistry.java
/**
* 将包下满足以superType为超类的Mapper接口及其对应的代理对象工厂注册到注册中心中
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
五、解析属性为resource或url的<mapper>元素
解析属性为resource或url的<mapper>元素时,和上面解析<package>元素和属性为class的<mapper>元素不太一样,这两者在解析的过程中,直接通过XMLMapperBuilder类的parse()方法解析对应的XML配置文件,在parse()方法中,首先通过configurationElement()方法实现XML配置的解析,然后又通过bindMapperForNamespace()方法去解析可能存在的注解配置,如果解析注解配置就回到上述解析属性为class时的<mapper>元素的流程。
入口:
if (resource != null && url == null && mapperClass == null) {
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
ErrorContext.instance().resource(url);
InputStream inputStream = Resources.getUrlAsStream(url);
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());
mapperParser.parse();
}
通过上述代码,我们可以清楚的看到,解析属性为resource或url时,都是先通过Resources获取对应映射文件的输入流,然后根据输入流,构建XMLMapperBuilder实例对象,然后在通过实例对象mapperParser的parse()方法进行解析过程。看到上面代码,我们会有一种很熟悉的感觉,因为在解析属性为class的<mapper>元素时,从第5步,执行MapperAnnotationBuilder.loadXmlResource()方法时,在loadXmlResource()方法中,就是通过构建XMLMapperBuilder实例对象,然后再通过实例对象mapperParser的parse()方法进行解析XML配置文件。
解析过程如下:
- XMLMapperBuilder.parse()方法
1>、首先判断configuration.isResourceLoaded()方法,如果返回false,继续执行。
2>、通过configurationElement()方法解析Mapper配置文件中的元素。
3>、为configration的loadedResources属性赋值,之后configuration.isResourceLoaded()方法将返回true。
4>、进入bindMapperForNamespace()方法 - XMLMapperBuilder.bindMapperForNamespace()方法
如果解析属性为resource或url的<mapper>元素时,只有对应的映射配置文件,而没有对应的接口类,所以这个时候 Resources.classForName(namespace);对应类实例为Null,所以,在该方法中不会执行任何的有效逻辑。执行完这个方法,整个解析过程也就完成了。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
if (!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
configuration.addLoadedResource("namespace:" + namespace);
configuration.addMapper(boundType);
}
}
}
}
六、总结
解析映射配置过程分为了XML类型映射配置和注解类型映射配置两种类型,其中注解类型映射配置又涉及到了面向接口编程的相关应用,具体可以结合本篇介绍和Mybatis的源码好好的体会。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/68884.html