官方文档——XML映射文件:mybatis.org/mybatis-3/z…
前言
映射文件
映射文件(Mapper)用于配置SQL语句,字段映射关系等。映射文件中包含 <cache>、<cache-ref>、<resultMap>、<sql>、<select|insert|update|delete> 等二级节点。
mapper标签
该标签的作用是加载映射文件的,加载方式有:
- 使用相对于类路径的资源引用
<!-- 使用相对于类路径的资源引用 -->
<mappers>
<mapper resource="org/mybatis/builder/AuthorMapper.xml"/>
</mappers>
- 使用完全限定资源定位符(URL)
<!-- 使用完全限定资源定位符(URL) -->
<mappers>
<mapper url="file:///var/mappers/AuthorMapper.xml"/>
</mappers>
- 使用映射器接口实现类的完全限定类名
<!-- 使用映射器接口实现类的完全限定类名 -->
<mappers>
<mapper class="org.mybatis.builder.AuthorMapper"/>
</mappers>
- 将包内的映射器接口实现全部注册为映射器
<!-- 将包内的映射器接口实现全部注册为映射器 -->
<mappers>
<package name="org.mybatis.builder"/>
</mappers>
解析类型
映射文件的解析过程是配置文件解析过程的一部分。解析逻辑封装在 mapperElement() 方法中。此方法的主要逻辑是遍历 <mappers> 的子节点,并根据节点属性值判断通过何种方式加载映射文件。
//mapperElement(root.evalNode("mappers"));
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 {
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();
}
// 使用完全限定资源定位符(URL)
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.");
}
}
}
}
}
解析XML映射文件
XMLMapperBuilder 是用于解析XML映射文件,其中 parser() 方法为解析入口。
从 <mapper> 节点开始解析
public void parse() {
//判断该映射文件是否已经被解析过
if (!configuration.isResourceLoaded(resource)) {
// 解析mapper节点
configurationElement(parser.evalNode("/mapper"));
// 添加映射文件路径到“已解析资源集合”中
configuration.addLoadedResource(resource);
// 通过命名空间绑定 Mapper 接口
bindMapperForNamespace();
}
// 处理未完成解析的节点
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
解析 <mapper> 下的节点
映射文件包含多种二级节点,比如 <cache>、<resultMap>、<sql>以及<select|insert|update|delete> 等。除此之外,还包含了一些三级节点,比如 <include>、<if>、<where> 等
<mapper namespace="com.xxx.dao.UserMapper">
<cache/>
<resultMap id="BaseUserResult" type="User">
<id property="id" column="id"/>
<result property="name" column="name"/>
<!-- ... -->
</resultMap>
<sql id="table">
user
</sql>
<select id="findOne" resultMap="BaseUserResult">
SELECT id, name, age FROM
<include refid="table"/>
WHERE id = #{id}
</select>
<!-- <insert|update|delete/> -->
</mapper>
解析映射文件节点的逻辑封装在相应的方法中,由 XMLMapperBuilder# configurationElement 方法提供解析入口。
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.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);
}
解析<select|insert|update|delete>
由 XMLMapperBuilder#buildStatementFromContext 方法完成解析操作。该方法是为每个 XNode对象创建一个 XMLStatementBuilder对象,具体解析操作委托给 XMLStatementBuilder#parseStatementNode方法完成。
private void buildStatementFromContext(List<XNode> list) {
if (configuration.getDatabaseId() != null) {
// 调用重载方法构建 Statement
buildStatementFromContext(list, configuration.getDatabaseId());
}
// 调用重载方法构建 Statement,requireDatabaseId 参数为空
buildStatementFromContext(list, null);
}
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
for (XNode context : list) {
// 创建Statment Builder类
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 解析 Statment 节点,并将解析结果存入到 Configuration的 mappedStatements 集合中
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
// 解析失败,将解析器放入Configuration的incompleteStatements集合中
configuration.addIncompleteStatement(statementParser);
}
}
}
解析 <select|insert|update|delete> 节点的各个属性并创建 MappedStatement 对象。
// XMLStatementBuilder
public void parseStatementNode() {
// 获取id 和 databaseId
String id = context.getStringAttribute("id");
String databaseId = context.getStringAttribute("databaseId");
// 根据当前节点的databaseId和目标databaseId进行检测,决定是否保存或者忽略当前节点的SQL语句
// databaseId MyBatis支持多厂商的功能
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取节点名称,比如 <select> 的节点名称为 select
String nodeName = context.getNode().getNodeName();
// 根据节点名称解析成 SqlCommandType
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> 节点
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);
// 解析 <selectKey> 节点
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 实例
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
// 创建 KeyGenerator 实例
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 解析 SQL 语句
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
// 解析 Statement 类型,默认为 PREPARED
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");
//通过别名解析 resultType 对应的类型
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");
// 构建 MappedStatement 对象,并将该对象存储到 Configuration 的 mappedStatements集合中
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
}
创建 SqlSource
SqlSource记录了 SQL 语句的信息。创建 SqlSource 是由 XMLLanguageDriver#createSqlSource 方法完成。方法内部将创建功能委托给 XMLScriptBuilder 类。
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
SQL 语句的解析逻辑封装在 XMLScriptBuilder#parseScriptNode 方法中。该方法首先会调用 parseDynamicTags() 解析 SQL 语句的节点,在解析过程中,会判断节点是是否包含一些动态标记,比如 ${} 占位符以及动态 SQL 节点等。若包含动态标记,则会将 isDynamic 设为 true。后续可根据 isDynamic 创建不同的 SqlSource。
// XMLScriptBuilder
public SqlSource parseScriptNode() {
// 解析 SQL 语句节点
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 根据 isDynamic 变量 创建不同的 SqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
parseDynamicTags() 主要是用来判断 SQL 语句是否包含一些动态标记,比如 ${} 占位符以及 动态SQL节点 等。这里,不管是 动态SQL节点 还是 静态SQL节点,MyBatis 都把它们看成是 SqlNode,一条 SQL 语句由多个 SqlNode 组成。在解析过程中,这些 SqlNode 被存储在 contents 集合中。
最后,该集合会被传给 MixedSqlNode 构造方法,用于创建 MixedSqlNode 实例。从 MixedSqlNode 类名上可知,它会存储多种类型的 SqlNode。
protected MixedSqlNode parseDynamicTags(XNode node) {
List<SqlNode> contents = new ArrayList<>();
NodeList children = node.getNode().getChildNodes();
// 遍历子节点
for (int i = 0; i < children.getLength(); i++) {
XNode child = node.newXNode(children.item(i));
if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) {
// 获取文本内容
String data = child.getStringBody("");
TextSqlNode textSqlNode = new TextSqlNode(data);
// 判断文本内容是否包含 动态节点标签 或 ${} 占位符
if (textSqlNode.isDynamic()) {
contents.add(textSqlNode);
isDynamic = true;
} else {
// 创建 StaticTextSqlNode
contents.add(new StaticTextSqlNode(data));
}
// child 节点是 ELEMENT_NODE 类型,比如 <if>、<where> 等
} else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) {
// 获取节点名称 如 if、where
String nodeName = child.getNode().getNodeName();
// 根据节点名称获取 对应的 NodeHandler
NodeHandler handler = nodeHandlerMap.get(nodeName);
if (handler == null) {
// 如果handler为空,表明当前节点不是MyBatis官方支持的节点,无法解析
throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement.");
}
// 处理 当前节点,生成相应的 SqlNode
handler.handleNode(child, contents);
isDynamic = true;
}
}
return new MixedSqlNode(contents);
}
NodeHandler
SqlNode 是通过 NodeHandler 类创建的。NodeHandler 是个接口,处理不同的SQL动态节点由不同的子类去实现。
// XMLScriptBuilder
private void initNodeHandlerMap() {
nodeHandlerMap.put("trim", new TrimHandler());
nodeHandlerMap.put("where", new WhereHandler());
nodeHandlerMap.put("set", new SetHandler());
nodeHandlerMap.put("foreach", new ForEachHandler());
nodeHandlerMap.put("if", new IfHandler());
nodeHandlerMap.put("choose", new ChooseHandler());
nodeHandlerMap.put("when", new IfHandler());
nodeHandlerMap.put("otherwise", new OtherwiseHandler());
nodeHandlerMap.put("bind", new BindHandler());
}
创建 MappedStatement
SQL 语句节点可以定义很多属性,这些属性和属性值以及 SqlSource 最终存储在 MappedStatement 中。
// MapperBuilderAssistant
public MappedStatement addMappedStatement(
String id,
SqlSource sqlSource,
StatementType statementType,
SqlCommandType sqlCommandType,
Integer fetchSize,
Integer timeout,
String parameterMap,
Class<?> parameterType,
String resultMap,
Class<?> resultType,
ResultSetType resultSetType,
boolean flushCache,
boolean useCache,
boolean resultOrdered,
KeyGenerator keyGenerator,
String keyProperty,
String keyColumn,
String databaseId,
LanguageDriver lang,
String resultSets) {
if (unresolvedCacheRef) {
throw new IncompleteElementException("Cache-ref not yet resolved");
}
id = applyCurrentNamespace(id, false);
boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
// 创建 Builder,设置各种属性
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,已经被官方废弃
ParameterMap statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
// 创建MappedStatement,并添加到 Configuration 中的 mappedStatements 集合中
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
绑定mapper接口
映射文件解析完成后,还需要通过命名空间绑定mapper接口,这样才能将映射文件中的sql语句和mapper接口中的方法绑定在一起。后续就可以直接通过调用mapper接口方法执行与之对应的sql语句。
private void bindMapperForNamespace() {
// 映射文件的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 根据命名空间生成 mapper接口类
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
// ignore, bound type is not required
}
// 检测当前 mapper接口类 是否被绑定过
if (boundType != null && !configuration.hasMapper(boundType)) {
configuration.addLoadedResource("namespace:" + namespace);
//绑定mapper类
configuration.addMapper(boundType);
}
}
}
最终调用 MapperRegistry#addMapper() 绑定mapper类
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 loadCompleted = false;
try {
// 将 type 和 MapperProxyFactory 进行绑定
// MapperProxyFactory 可为 mapper 接口生成代理类
knownMappers.put(type, new MapperProxyFactory<>(type));
// 创建注解解析器。在MyBatis中,有XML和注解2种配置方式可选
MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type);
// 解析注解中的信息
parser.parse();
loadCompleted = true;
} finally {
if (!loadCompleted) {
knownMappers.remove(type);
}
}
}
}
Mapper接口绑定过程:
- 获取命名空间,并根据命名空间生成
mapper接口类。 - 将
type和MapperProxyFactory实例存入knownMappers中。 - 解析注解中的信息。
处理未完成解析的节点
在解析某些节点的过程中,如果这些节点引用了其他一些未被解析的配置,会导致当前 节点解析工作无法进行下去。对于这种情况,MyBatis 的做法是抛出 IncompleteElementException 。外部逻辑会捕捉这个异常,并将节点对应的解析器放入 Configuration中的 incomplet* 集合中。
解析未完成的 <cache-ref> 节点
假设MyBatis先解析映射文件1,再解析映射文件2。按照这样的解析顺序,映射文件1中的<cache-ref>节点就无法完成解析,因为它所引用的缓存还未被解析。
<!-- 映射文件1 -->
<mapper namespace="com.xxx.dao.UserMapper1">
<cache-ref namespace="com.xxx.dao.UserMapper2"/>
</mapper>
<!-- 映射文件2 -->
<mapper namespace="com.xxx.dao.UserMapper2">
<cache/>
</mapper>
当映射文件2解析完成后,MyBatis 会调用 parsePendingCacheRefs() 方法处理在此之前未完成解析的节点。
private void parsePendingCacheRefs() {
// 获取 CacheRefResolver 列表
Collection<CacheRefResolver> incompleteCacheRefs = configuration.getIncompleteCacheRefs();
synchronized (incompleteCacheRefs) {
Iterator<CacheRefResolver> iter = incompleteCacheRefs.iterator();
// 通过迭代器遍历列表
while (iter.hasNext()) {
try {
// 尝试解析 <cache-ref> 节点,若解析失败,则抛处 IncompleteElementException,此时下面的remove操作不会执行。
iter.next().resolveCacheRef();
// 移除 CacheRefResolver 对象。代表已经成功解析 <cache-ref> 节点
iter.remove();
} catch (IncompleteElementException e) {
// 如果再次捕获 IncompleteElementException 异常,表示当前映射文件中并没有<cache-ref>所引用的缓存。有可能所引用的缓存在后面的映射文件中。所以需要保留 CacheRefResolver。
}
}
}
}
解析步骤:
- 获取获取
CacheRefResolver列表,并进行遍历 - 尝试解析节点,若解析失败再次抛出异常
- 若解析成功则列表中移除相关节点