文章目录
前言
在上一章节当中我们主要是讲解了MyBatis工作流程初始化阶段的第一步,解析配置文件。解析配置文件主要使用的XMLConfigBuilder,通过这个Builder解析xml文件然后注册相关信息到configuration当中,除mappers之外的每一个节点的解析还是非常简单的,除了前后的相互依赖关系之外,倒也没啥特别的,而到了最后一步,才是真正的繁杂之处了,因为所为的mappers引入的是另一个完整的xml文件。
// 解析mappers节点 同样支持多文件扫描和单文件查找
mapperElement(root.evalNode("mappers"));
本章就来分析MyBatis是如何解析mapper对应的xml文件。
mapper解析目标
mapper解析目标与Config配置文件的解析目标大方向上是一致的,就是读取xml内容并填充到Configuration当中,具体来说则涉及到Configuration当中的两大Map属性:resultMaps和mappedStatements。
protected final Map<String, ResultMap> resultMaps = new StrictMap<ResultMap>("Result Maps collection");
// 以下采用StrictMap保证key值不会重复注册
protected final Map<String, MappedStatement> mappedStatements = new StrictMap<MappedStatement>(
"Mapped Statements collection");
可以看到这里有两个类:ResultMap和MappedStatement,这两个类是啥呢?
其实在mapper当中最重要的莫过于resultMap、select、delete、update、insert这五个子元素了,而后面四个在MyBatis中又可以统一为执行语句类,所以最后将resultmap解析为ResultMap对象,而其他的四类节点都解析为MappedStatement对象。比如在上面的xml文件中,就会解析出一个ResultMap对象和三个MappedStatement对象。ResultMap如下图所示
MappedStatement如下图所示
而其他的节点也会在Configuration当中对应某个属性,比如二级缓存对应org.apache.ibatis.session.Configuration#caches属性,但真实查询时使用的还是MappedStatement当中的cache属性,在Configuration#caches存储是用于解析cache-ref标签时方便查询的,可以说只是一个解析过程中的中间产物。
对于sql节点虽然在Configuration也有单独的记录,但是它跟caches节点一样也是一个中间产物,在通过include元素引用这个sql片段的select、delete等节点解析过程中会引用到这个属性并将节点内容替换。也就是其实初始化之后这个属性也就是没有价值了。
这个阶段除了xml文件的解析之外,还有一个非常重要的就是查找xml命名空间所对应的接口并注册。这里不是注册作为Configuration的某个属性,而是通过它的属性mapperRegistry进行注册的,最后相关的接口以及映射关系在org.apache.ibatis.binding.MapperRegistry#knownMappers属性当中。在个knownMappers非常重要。毕竟我们在使用MyBatis时是面向接口的。
所以整个mapper文件解析的最终目标最重要的三点就是:
1. 解析resultMap元素为org.apache.ibatis.mapping.ResultMap对象并注册到Configuration#resultMaps属性当中
2. 解析select、update、delete、insert元素为org.apache.ibatis.mapping.MappedStatement对象并注册到Configuration#mappedStatements属性当中。
3. 初始化mapper文件命名空间对应的类并通过Configuration#mapperRegistry进行注册
mapper解析流程
在MyBatis的配置文件当中,配置需要解析的mapper.xml文件的方式有两种方式:第一种通过扫描指定文件夹的方式,第二种通过指定扫描文件的方式。
第一种方式如下
<mappers>
<package name="sample.mybatis.mapper"/>
</mappers>
对应的源码如下:org.apache.ibatis.builder.xml.XMLConfigBuilder#mapperElement
private void mapperElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
if ("package".equals(child.getName())) {
String mapperPackage = child.getStringAttribute("name");
configuration.addMappers(mapperPackage);
} else {
... 这里为第二种方式
}
}
}
}
此时会直接调用mapperRegistry注册包的方式
public void addMappers(String packageName) {
mapperRegistry.addMappers(packageName);
}
/**
* @since 3.2.2
*/
public void addMappers(String packageName) {
addMappers(packageName, Object.class);
}
/**
* @since 3.2.2
*/
public void addMappers(String packageName, Class<?> superType) {
1. 解析包路径下符合条件的类 这个的IsA是要求目标类都必须是superType的子类
ResolverUtil<Class<?>> resolverUtil = new ResolverUtil<Class<?>>();
resolverUtil.find(new ResolverUtil.IsA(superType), packageName);
2. 获取符合以上条件的类
Set<Class<? extends Class<?>>> mapperSet = resolverUtil.getClasses();
3. 遍历以上的类 并执行注册
for (Class<?> mapperClass : mapperSet) {
addMapper(mapperClass);
}
}
// 解析mapper.xml文件时或者mapper接口时增加
public <T> void addMapper(Class<T> type) {
1. 首先这个类必须是个接口
if (type.isInterface()) {
2. 如果这个类已经解析过 会报错 不允许重复注册
if (hasMapper(type)) {
throw new BindingException("Type " + type + " is already known to the MapperRegistry.");
}
boolean loadCompleted = false;
try {
3. 保存mapper和代理工厂的映射关系
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. // mapper parser可能会尝试绑定
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
4. 解析mapper接口上的注解并尝试读取默认的xml文件
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
如果对上一章VFS还有印象的话,不难看出这里其实就是扫描指定包路径下面的所有类,然后尝试解析这个类。其实这种方式是在3.2.2版本才加入的,主要的目的是因为要在接口上面增加注解,通过完全使用注解的方式可以不需要xml文件了,而执行注解解析操作是通过MapperAnnotationBuilder这个类来完成的。当然了这里还是会支持xml文件的,在MapperAnnotationBuilder#parse方法当中会通过loadXmlResource来加载按照一定规则存放和命名的文件的。比如接口名称为sample.mybatis.mapper.HotelMapper,那么对应的xml资源路径为sample/mybatis/mapper/HotelMapper.xml。如果将这个文件存放在其他路径下,就不会找到对应xml文件了。如果既没有注解,也不按照规则存放xml文件,在初始化的过程中并不会报错,直到真正运行无法初始化mapper接口方法的时候就会以下的异常
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): sample.mybatis.mapper.CityMapper.findAll
at org.apache.ibatis.binding.MapperMethod$SqlCommand.<init>(MapperMethod.java:228)
at org.apache.ibatis.binding.MapperMethod.<init>(MapperMethod.java:49)
at org.apache.ibatis.binding.MapperProxy.cachedMapperMethod(MapperProxy.java:66)
at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:59)
at com.sun.proxy.$Proxy8.findAll(Unknown Source)
at sample.mybatis.MyBatisTest.testBuilder(MyBatisTest.java:34)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
所以,第一种按照包路径加载的方式不是指定xml资源的路径,而是指定mapperInterface的包名。
第二种方式,通过扫描指定文件的方式,既可以指定xml资源的路径,也可以指定对应mapper接口。
<mappers>
1. 指定mapper接口的方式
<mapper class="sample.mybatis.mapper.CityMapper"/>
2. 通过resource相对路径指定mapper.xml文件的路径
<mapper resource="sample/mybatis/mapper/CityMapper.xml"/>
3. 通过url指定mapper.xml文件的路径
<mapper url="file:D:\mybatis\parent-mybatis-parent-29\spring-boot-starter-mybatis-spring-boot-1.3.2\mybatis-spring-boot-samples\mybatis-spring-boot-sample-xml\src\main\resources\sample\mybatis\mapper\HotelMapper.xml"/>
</mappers>
源码如下
String resource = child.getStringAttribute("resource");
String url = child.getStringAttribute("url");
String mapperClass = child.getStringAttribute("class");
if (resource != null && url == null && mapperClass == null) {
1. 按照resource资源路径加载文件并解析
ErrorContext.instance().resource(resource);
InputStream inputStream = Resources.getResourceAsStream(resource);
// mapper文件解析器 此时会构造一个XPathParser 并解析当前xml为一个Document对象
// 同时会设置一个解析该xml过程中唯一的一个builderAssistant对象
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments());
mapperParser.parse();
} else if (resource == null && url != null && mapperClass == null) {
2. 按照url路径加载文件并解析
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) {
3. 按照直接注册mapper接口的方式
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.");
}
第三种与上面通过扫描包的方式其实是一样的,而通过resource和通过url的差别只不过是获取流的方式不同,真正解析的流程是一致的。如下所示:
1. 创建mapper文件的解析构造器
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource,
configuration.getSqlFragments());
2. 进行解析操作
mapperParser.parse();
构造mapperParser的过程源码如下
public XMLMapperBuilder(InputStream inputStream, Configuration configuration, String resource,
Map<String, XNode> sqlFragments) {
1. 首先创建XPathParser并创建document
this(new XPathParser(inputStream, true, configuration.getVariables(), new XMLMapperEntityResolver()),
configuration, resource, sqlFragments);
}
private XMLMapperBuilder(XPathParser parser, Configuration configuration, String resource,
Map<String, XNode> sqlFragments) {
super(configuration);
1. 每个XMLMapperBuilder对应一个builderAssistant
this.builderAssistant = new MapperBuilderAssistant(configuration, resource);
this.parser = parser;
this.sqlFragments = sqlFragments;
this.resource = resource;
}
通过以上的构造器我们不难得出以下的结论:每一个xml文件都对应一个XMLMapperBuilder对象,而这个对象内部都包含一个XPathParser对象和一个MapperBuilderAssistant对象,XPathParser在上一章中我们已经介绍过,用于解析xml文件为Document,并且在读取时将节点转为XNode,所以mapper文件同样支持占位符模式(每个XMLMapperBuilder对象之间是共享configuration,所以也共享在配置文件中定义的变量)。而MapperBuilderAssistant这里是首次出现,但是在后面解析的过程中会起到相当重要的作用。在mapper文件的解析过程中,通过XPathParser读取文件创建好Document之后,通过XMLMapperBuilder读取节点的属性,然后再通过MapperBuilderAssistant进行一定的处理然后再注册到configuration当中。以下为XMLMapperBuilder解析的主流程
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
1. 解析mapper节点 也就是解析mapper.xml的关键 解析mapper节点 填充要素到configuration当中
configurationElement(parser.evalNode("/mapper"));
2. 记录已经解析过的mapper资源
configuration.addLoadedResource(resource);
3. 根据namespace查找对应的接口 并通过mapperRegistry进行注册
bindMapperForNamespace();
}
4. 尝试解析尚未解析完全的ResultMap信息
parsePendingResultMaps();
5. 尝试解析尚未解析完全的Cache-Ref信息
parsePendingCacheRefs();
6. 尝试解析尚未解析完全的XMLStatementBuilder
parsePendingStatements();
}
其中的重点是第一步(解析mapper文件并注册)和第三步(注册mapper接口)。
- 解析mapper节点
解析xml文件无非就是按照节点一个一个读取,并根据节点的不同封装成不用对象,然后再填充到Configuration当中。
// 解析mapper文件
private void configurationElement(XNode context) {
try {
1. 获取命名空间 设置当前命名空间 保证全局唯一性
String namespace = context.getStringAttribute("namespace");
if (namespace == null || namespace.equals("")) {
throw new BuilderException("Mapper's namespace cannot be empty");
}
builderAssistant.setCurrentNamespace(namespace);
2. 解析二级缓存引用 引用另一个mapper的缓存 因此由可能两个mapper共享一个二级缓存
cacheRefElement(context.evalNode("cache-ref"));
3. 解析二级缓存 最后将二级缓存保存到builderAssistant的currentCache属性和Configuration的caches属性当中
cacheElement(context.evalNode("cache"));
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
4. 解析resultMap节点 因为涉及内嵌resultMap的情形(collection或者association)
resultMapElements(context.evalNodes("/mapper/resultMap"));
5. 解析sql片段
// <sql id="baseStatement">
// ci.city_id, ci.name as city_name, ci.state
// </sql>
sqlElement(context.evalNodes("/mapper/sql"));
6. 解析Statement
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);
}
}
以上步骤中最复杂也最关键的是第四步和第六步,分别为解析resultMap和select|insert|update|delete标签。在XMLMapperBuilder方法中相关的方法中我们分析其中的buildResultMappingFromContext
/**
* <resultMap id="BaseResultMap" type="sample.mybatis.domain.City">
* <id property="id" column="id" jdbcType="INTEGER"/>
* <result property="name" column="name" jdbcType="VARCHAR"/> javaType和jdbcType用于解析TypeHandler
* <result property="state" column="state" jdbcType="VARCHAR"/>
* <result property="country" column="country" jdbcType="VARCHAR"/>
* </resultMap>
*
* @param context resultMap中的子节点 id和result
* @param resultType sample.mybatis.domain.City
* @param flags 比如id为ID 或者构造器参数 constructor
* @return <result property="name" column="name" jdbcType="VARCHAR"/> -> ResultMapping
* @throws Exception
*/
private ResultMapping buildResultMappingFromContext(XNode context, Class<?> resultType, List<ResultFlag> flags)
throws Exception {
String property;
if (flags.contains(ResultFlag.CONSTRUCTOR)) {
property = context.getStringAttribute("name");
} else {
property = context.getStringAttribute("property");// 对象属性名称
}
String column = context.getStringAttribute("column");// 数据库字段
String javaType = context.getStringAttribute("javaType");
String jdbcType = context.getStringAttribute("jdbcType");
// 当前元素包含有select属性 <collection property="posts" column="id" ofType="Post" select="selectPostsForBlog"/>
String nestedSelect = context.getStringAttribute("select");
// 解析association和collection元素 但不包含select属性
// <association property="country" javaType="sample.mybatis.domain.Country">
// <id property="id" column="country_id" jdbcType="INTEGER"/>
// <result property="name" column="country_name" jdbcType="VARCHAR"/>
// <result property="continent" column="continent" jdbcType="VARCHAR"/>
// </association>
String nestedResultMap = context.getStringAttribute("resultMap",
processNestedResultMappings(context, Collections.<ResultMapping>emptyList()));// association collection
String notNullColumn = context.getStringAttribute("notNullColumn");
String columnPrefix = context.getStringAttribute("columnPrefix");
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"));
Class<?> javaTypeClass = resolveClass(javaType);
@SuppressWarnings("unchecked")
Class<? extends TypeHandler<?>> typeHandlerClass = (Class<? extends TypeHandler<?>>) resolveClass(typeHandler);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum,
nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet,
foreignColumn, lazy);
}
一个resultMap其实就是对一个类的描述,而其中的属性定义则是resultMap中的各个节点来表示,比如id、result、constructor、association和collection,在MyBatis当中resultMap最后会抽象为ResultMap对象,而这些子元素会抽象为ResultMapping对象,上面的方法就是用于解析resultMap的这些子节点为ResultMapping对象的。其中的id和constructor给对应的属性赋予了额外的含义,所以在flags属性当中会记录相关信息。
id 和 result 元素都将一个列的值映射到一个简单数据类型(String, int, double, Date 等)的属性或字段。这两者之间的唯一不同是,id 元素对应的属性会被标记为对象的标识符,在比较对象实例时使用。 这样可以提高整体的性能,尤其是进行缓存和嵌套结果映射(也就是连接映射)的时候。
而association和collection元素通常代表的不是一个普通的java属性,要么包含了子元素,要么包含了其他的查询语句,也就是说嵌套结果或者嵌套查询,在MyBatis中同样也将这两种节点作为ResultMap处理,同时也是作为一个ResultMapping对象记录到父resultMap中。而在ResultMapping的属性nestedResultMapId或者nestedQueryId中记录相关信息。前者记录的嵌套结果信息,后者记录的是嵌套查询的信息。
在上面的buildResultMappingFromContext方法的主要做的就是读取节点的属性,除了processNestedResultMappings又会递归调用按照resultMap的标签处理之外,没啥复杂的,就是收集相关的信息,最后交给builderAssistant来处理,
/**
* 对应resultMap中的result子元素 包含ID, CONSTRUCTOR
* <p>
* <id property="id" column="id" javaType="long" jdbcType="BIGINT" typeHandler="org.apache.ibatis.type
* .LongTypeHandler"/>
* <result property="name" column="name" jdbcType="VARCHAR"/>
*
* @param resultType 类的类型
* @param property 属性名称
* @param column 属性名称对应的数据库字段名称
* @param javaType 属性类型
* @param jdbcType 数据库字段类型
* @param nestedSelect 是否嵌套查询
* @param nestedResultMap 嵌套的结果集 比如association和collection元素
* @param notNullColumn
* @param columnPrefix 数据库字段前缀 column映射数据库查询语句返回字段包含的前缀
* @param typeHandler 用户设置的typeHandler 用于setParameter和getResult时使用
* @param flags ID, CONSTRUCTOR信息
* @param resultSet
* @param foreignColumn
* @param lazy
* @return
*/
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) {
1. 根据反射类和属性名称解析属性类型
Class<?> javaTypeClass = resolveResultJavaType(resultType, property, javaType);
2. 尝试解析出TypeHandler
TypeHandler<?> typeHandlerInstance = resolveTypeHandler(javaTypeClass, typeHandler);
List<ResultMapping> composites = parseCompositeColumnName(column);
3. 根据收集的信息构造ResultMapping对象
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<ResultFlag>() : flags)
.composites(composites).notNullColumns(parseMultipleColumnNames(notNullColumn))
.columnPrefix(columnPrefix).foreignColumn(foreignColumn).lazy(lazy).build();
}
builderAssistant做的事情就是将以上收集的信息再处理一下,比如解析属性的类型,根据属性类型查找对应的TypeHandler,最后通过建造者模式构造一个目标对象。其实处理resultMap的流程和上面是一模一样的,首先在XMLMapperBuilder#resultMapElements中收集属性信息和子节点,最后构造一个ResultMapResolver对象,这个对象也不过是对收集的属性信息、builderAssistant的简单包装,而调用resolve方法真实调用的就是MapperBuilderAssistant#addResultMap方法。
MapperBuilderAssistant的处理跟上面的ResultMapping的方式也差不多,适度解析,然后构造目标对象,这里是ResultMap对象,同样是通过建造者模式。不过比前面要多一步,将构造好的ResultMap对象记录到configuration当中。
通过以上的步骤,最终完成了resultMap的解析。在上面我们说过,对于association、collection也会作为一个ResultMap进行处理,在ResultMapping中只是通过一个属性关联,而真正的信息也是通过跟以上同样的方式注册到configuration当中的。
接下来分析一下select|insert|update|delete这些标签的处理方式。MyBatis主要是通过XMLStatementBuilder来处理这些标签的,通过构造一个MappedStatement对象。整个过程中有如下几个点比较复杂,首先就是要将其中的sql语句解析放到MappedStatement的sqlSource当中,而且将其中包含的#{}
替换为?
,必要的情况下还要存储传入的参数映射。
而这一操作是通过LanguageDriver来完成的,而LanguageDriver又实际上交给了XMLScriptBuilder来处理,为什么要这么复杂,因为实际解析过程中涉及到很多动态语句,比如if、trim这些标签。除了这些复杂性之外,还有include标签。在语句当中可以通过include标签引入其他sql定义的语句片段,MyBatis在解析过程中首先通过XMLIncludeTransformer将include片段替换掉,而处理过程中替换的操作又必须在上面解析为sqlSource之前完成。如下所示
处理include还是简单的,通过refid找到对应的sql片段然后替换到对应的元素当中即可。
替换的完整结果如下图
另外,在上上图的源码当中,我们还可以发现在处理include和解析为sqlSource之间还有一个需要处理的是selectKey
标签。MyBatis的处理方式是将这个selectKey
对应的元素也作为一个MapperdStatement对象。
父标签元素对应
要掌握MappedStatement的解析过程其实就是对这个对象属性的认知,在上面我们已经介绍了sqlSource、keyGenerator、lang,另外在前面篇章介绍缓存时也介绍过二级缓存cache,在日志增强章节也介绍了statementLog。其实在MappedStatement中还有一个更重要的属性resultMaps,对于查询语句无论是定义了resultMap或是resultType属性,都会在这个属性当中存放关联的ResultMap对象。比如不存在resultMap属性但存在resultType属性的
已经存在resultMap属性的直接关联即可。
这一块的实现参考源码org.apache.ibatis.builder.MapperBuilderAssistant#getStatementResultMaps
private List<ResultMap> getStatementResultMaps(String resultMap, Class<?> resultType, String statementId) {
resultMap = applyCurrentNamespace(resultMap, true);
List<ResultMap> resultMaps = new ArrayList<ResultMap>();
1. resultMap属性不为空 通过configuration查找指定id的ResultMap对象
if (resultMap != null) {
String[] resultMapNames = resultMap.split(",");
for (String resultMapName : resultMapNames) {
try {
resultMaps.add(configuration.getResultMap(resultMapName.trim()));
} catch (IllegalArgumentException e) {
throw new IncompleteElementException("Could not find result map " + resultMapName, e);
}
}
} else if (resultType != null) {
2. resultType属性不为空 则创建一个内联的ResultMap对象
ResultMap inlineResultMap = new ResultMap.Builder(configuration, statementId + "-Inline", resultType,
new ArrayList<ResultMapping>(), null).build();
resultMaps.add(inlineResultMap);
}
return resultMaps;
}
通过以上步骤之后,完成了MappedStatement对象的构造之后,就可以注册到Configuration的mappedStatements属性当中了。
总结一下,针对整个mapperXml文件的解析首先都是由对应的Builder读取参数、然后适度的解析,然后交给builderAssistant构造成目标对象然后注册到Configuration中的指定属性中,这其中最重要的无非就是ResultMap和MappedStatement对象。
- 注册mapper接口
初始化阶段走到了现在,绝大部分工作已经完成了,但是还差临门一脚。为啥这么说呢?我们平时在使用MyBatis的时候其实是面向接口的,如果我们解析完了mapperXml文件,但我们的接口该如何关联到对应的xml呢?所以在org.apache.ibatis.builder.xml.XMLMapperBuilder#parse中会调用bindMapperForNamespace方法,根据xml的命名空间查找目标类,并添加到configuration当中。
private void bindMapperForNamespace() {
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
1. 根据命名空间查找目标类
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
if (boundType != null) {
2. 如果还没有注册到configuration当中 则进行注册
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);
3. 添加mapper接口 此时会在org.apache.ibatis.binding.MapperRegistry.knownMappers添加接口与MapperProxyFactory的映射
configuration.addMapper(boundType);
}
}
}
}
而configuration.addMapper恰好和扫描文件夹注册接口的方式其实就是一样的。通过以上步骤最后将一个mapper接口与MapperProxyFactory关联,在实际使用时可以通过MapperProxyFactory来获取对应接口的具体实现进行操作了,当然这些不是在初始化阶段完成的。
总结
MyBatis对于mapperXml文件的解析过程其实是简单的,跟流水线一样,只是细节比较多。整体流程是通过对应的Builder读取xml中的节点,读取节点属性和子节点,进行适度解析,关联其他元素,最后交给builderAssistant构造成目标对象并注册到configuration中。完成了xml文件的解析,还需要将接口也注册到configuration当中。结合上一章,无论是配置文件还是mapper文件,包括对应的接口信息,最后都存放到了configuration属性当中,后续的任何操作我们就只用面对configuration属性了。另外在MyBatis针对xml解析并构造对象的过程中大量使用了建造者模式,这种模式与工厂模式都可以创建对象并为客户端屏蔽复杂度,但是使用的场景不同,用一句话形容二者的差别就是:**建造者模式生产定制版,而工厂模式生产大众版**
。建造者的应用场景如下:
- 需要生成的对象具有复杂的内部结构,实例化对象需要屏蔽对象内部的细节,让上层代码与复杂对象的实例化过程解耦,可以使用建造者模式;简而言之,如果遇到多个构造器参数时要考虑建造者模式。
- 对象的实例化需要依赖各个组件的生产以及装配顺序,关注的是一步一步地组装出目标对象,可以使用建造者模式。