6.XML文件格式的mapper标签解析
上一节已经知道,对于XML文件中mapper
标签的解析都是通过XMLMapperBuilder
进行处理的。
接下来让我们首先对XMLMapperBuilder
进行分析,然后再详细考察mapper
标签的解析逻辑。
XMLMapperBuilder
XMLMapperBuilder
,顾名思义,该工具类是用于解析mapper
标签的,这里我们主要分析XMLMapperBuilder
的四个属性,便于下面分析具体的业务逻辑。XMLMapperBuilder
拥有如下四个属性:
- XPathParser parser:XML文件解析器,该对象是XML文件解析的工具类。
- MapperBuilderAssistant builderAssistant:Mapper构建协助器,由于Mybatis中的很多对象都比较大,属性比较多,而解析结果本身就是一个对象,因此Mybatis将解析结果的构建代码进行封装,封装到这个构建协助器里,让代码更加简洁。除此之外,这里还存储了当前的命名空间,方便获取。
- Map<String, XNode> sqlFragments :Sql段,保存
<mapper>
标签的<sql>
子标签定义的sql语句,这里我们先不考虑该对象含义。 - String resource:要解析的资源的名称
开始解析mapper标签
了解了XMLMapperBuilder的各个属性的含义,就可以开始解析mapper
标签了,否则我们可能在解析到一半就不知道哪个东西是做什么的了。
根据XMLConfigBuilder
的mapperElement(XNode)
方法中如下代码:
XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());
mapperParser.parse();
我们知道,mapper
标签的解析是由XMLMapperBuilder
的parse()
方法完成的。让我们考察该方法代码:
public void parse() {
// 如果资源没有被解析过,则需要进行解析
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
// 解析未完成解析的ResultMap、CacheRef和Statement
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
对于该方法的前半部分肯定大家都没有意见,如果资源没有被解析,就开始解析,这是一个正常的需求,而后半部分,则就无法理解了,为什么这里无论如何都要执行呢?事实上,并不是解析了一个mapper
配置文件就可以获取到所有的ResultMap
、CacheRef
和Statements
的,所以再每次解析完一个mapper
配置文件都会进行解析一遍上面三个资源,而且,对于具有Java配置的Mybatis,可能还需要解析完Java配置后,再去解析上面的三个资源。所以才会出现后半部分。
让我们先讨论前半部分的解析mapper
标签的部分。其中:
-
configurationElement(parser.evalNode("/mapper"))
这行代码负责主要解析mapper配置,并创建一个MappedStatement
放入到Configuration
类型的对象中。 -
configuration.addLoadedResource(resource);
这行代码负责记录,该配置文件已经被解析完毕 -
bindMapperForNamespace();
最后的一行代码负责将Java的Mapper
对象与命名空间(即XML配置文件)联系起来。
解析mapper标签
下面让我们考察一下configurationElement(XNode)
方法。该方法是解析XML映射文件
的主要逻辑。代码如下:
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"));
// 解析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>
标签,该标签是引用其他缓存的标签,Mybatis对该标签描述如下:
对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新。 但你可能会想要在多个命名空间中共享相同的缓存配置和实例。要实现这种需求,你可以使用 cache-ref 元素来引用另一个缓存。
该属性的解析由cacheRefElement(XNode)
方法处理,其实代码很简单:
private void cacheRefElement(XNode context) {
if (context != null) {
// 在Configuration的缓存引用注册表中添加一条
// 当前命名空间:要引用的命名空间
// 用于标识命名空间缓存之间的引用关系
configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));
// 将builderAssistant和要引用的命名空间缓存封装入一个CacheRefResolver去解析缓存
// 解析成功后放到builderAssistant备用
CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));
try {
// 尝试解析缓存
// 解析失败则放入到Configuration对象的incompleteCacheRefs注册表中
cacheRefResolver.resolveCacheRef();
} catch (IncompleteElementException e) {
configuration.addIncompleteCacheRef(cacheRefResolver);
}
}
}
事实证明,在Mybatis中,储存命名空间使用缓存关系的是一张名为cacheRefMap
格式为<String,String>
的注册表,其中key和value都是命名空间的名称。因此此处,是将<当前命名空间,引用的缓存的命名空间>
放入到注册表中。然后再根据缓存命名空间解析使用的缓存。
当然对于解析失败的缓存,Mybatis将解析失败,即当前未生成的缓存,就将其放入到incompleteCacheRefs
注册表中,等待进一步解析,这也就是mapper
标签解析中后三个方法的意义。
解析cache标签
<cache>
标签用于定义当前命名空间使用的缓存,该标签的解析是由cacheElement(XNode)
方法完成的。解析方法相对简单:
private void cacheElement(XNode context) {
if (context != null) {
// 获取缓存类型
// 默认情况下是永久缓存,没有过期时间
String type = context.getStringAttribute("type", "PERPETUAL");
Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);
// 解析过期策略,默认是LRU,最近最少使用
String eviction = context.getStringAttribute("eviction", "LRU");
Class<? extends Cache> evictionClass = typeAliasRegistry.resolveAlias(eviction);
// 设置刷新间隔
Long flushInterval = context.getLongAttribute("flushInterval");
// 设置缓存大小
Integer size = context.getIntAttribute("size");
// 设置缓存是否只读
boolean readWrite = !context.getBooleanAttribute("readOnly", false);
boolean blocking = context.getBooleanAttribute("blocking", false);
// 将所有属性按照Properties读取
Properties props = context.getChildrenAsProperties();
// 将读取到的属性应用到缓存中,构建缓存
builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);
}
}
之前说过,MapperBuilderAssistant
的一大功能就是创建复杂对象并注册到Configuration
注册表中,这里就是通过读取到的属性建立一个缓存,然后注册到Configuration对象中。查看MapperBuilderAssistant.useNewCache(...)
方法:
public Cache useNewCache(Class<? extends Cache> typeClass,
Class<? extends Cache> evictionClass,
Long flushInterval,
Integer size,
boolean readWrite,
boolean blocking,
Properties props) {
// 根据配置构建Cache
// 默认状态是永久缓存,使用LRU算法
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;
}
那么在Mybatis中,缓存是如何存储的呢?考察如下代码:
configuration.addCache(cache);
public void addCache(Cache cache) {
caches.put(cache.getId(), cache);
}
可以看到,缓存在Configuration对象中也是存放在一个注册表中,该注册表叫做caches
,格式是<String, Cache>
,其中key是命名空间,value是缓存实例。
解析parameterMap标签
<parameterMap>
标签用于声明方法参数类型,解析该标签使用的是parameterMapElement(List<XNode>)
方法,考察该方法:
private void parameterMapElement(List<XNode> list) {
// 遍历所有parameterMap标签
for (XNode parameterMapNode : list) {
// 获取parameterMap的id
String id = parameterMapNode.getStringAttribute("id");
// 获取parameterMap代表的Class类型
String type = parameterMapNode.getStringAttribute("type");
// 将type解析为Class对象
Class<?> parameterClass = resolveClass(type);
// 获取所有parameter子标签
List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
// 创建列表存储子标签内容
List<ParameterMapping> parameterMappings = new ArrayList<>();
// 遍历所有子标签
for (XNode parameterNode : parameterNodes) {
// 获取属性名
String property = parameterNode.getStringAttribute("property");
// 获取属性类型
String javaType = parameterNode.getStringAttribute("javaType");
// 获取属性代表的JDBC类型
String jdbcType = parameterNode.getStringAttribute("jdbcType");
// 获取resultMap属性,具体使用到的情况请看Mybatis文档
String resultMap = parameterNode.getStringAttribute("resultMap");
// 获取mode属性
String mode = parameterNode.getStringAttribute("mode");
// 获取typeHandler属性
String typeHandler = parameterNode.getStringAttribute("typeHandler");
// 获取numericScale属性,该属性表示小数保留的位数
Integer numericScale = parameterNode.getIntAttribute("numericScale");
ParameterMode modeEnum = resolveParameterMode(mode);
Class<?> javaTypeClass = resolveClass(javaType);
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
// 将子标签构造成一个ParameterMapping
ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
// 并放入一个列表中
parameterMappings.add(parameterMapping);
}
// 最后将结果列表注册到`Configuration`对象的`parameterMaps`属性中
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
}
}
仔细考察builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
可以发现,Configuration
对象使用parameterMaps
注册表存储各个ParameterMap
,格式为<String, ParameterMap>
。其中key是id
,parameterMap的id是命名空间 + id字段
,value就是parameterMap对象。
解析resultMap标签
<resultMap>
标签用于表示Mybatis数据库操作的结果集,是Mybatis引以为豪的一部分。该标签功能相当强大,在一般的结果映射上增添了许多功能。ResultMap 的设计思想是,对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了。
解析<resultMap>
标签的方法是resultMapElements(List<XNode>)
,该方法只是遍历所有的<resultMap>
标签对其进行解析罢了:
private void resultMapElements(List<XNode> list) throws Exception {
for (XNode resultMapNode : list) {
try {
resultMapElement(resultMapNode);
} catch (IncompleteElementException e) {
// ignore, it will be retried
}
}
}
真正的解析方法其实在resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings, Class<?> enclosingType)
中,因为resultMapElement(resultMapNode);
就是调用的该方法,我们直接考察该方法:
private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {
ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());
// 获取resultMap的id
String id = resultMapNode.getStringAttribute("id",
resultMapNode.getValueBasedIdentifier());
// 获取resultMap的java类型
// 获取顺序是type->ofType->resultType->javaType
String type = resultMapNode.getStringAttribute("type",
resultMapNode.getStringAttribute("ofType",
resultMapNode.getStringAttribute("resultType",
resultMapNode.getStringAttribute("javaType"))));
// 获取extends属性,
String extend = resultMapNode.getStringAttribute("extends");
// 获取autoMapping属性
Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");
// 通过获取到的Java类型获取对应的Class对象
Class<?> typeClass = resolveClass(type);
Discriminator discriminator = null;
List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();
resultMappings.addAll(additionalResultMappings);
// 遍历resultMap标签的所有子标签
// resultMap的子标签共有4种:
// 1. constructor:用于声明创建结果集的构造器
// 2. discriminator:配置鉴别器
// 3. id:用于标记字段是id
// 4. result:用于标记属性与数据库字段的对应关系
List<XNode> resultChildren = resultMapNode.getChildren();
for (XNode resultChild : resultChildren) {
if ("constructor".equals(resultChild.getName())) {
// connstuctor标签的解析
processConstructorElement(resultChild, typeClass, resultMappings);
} else if ("discriminator".equals(resultChild.getName())) {
// discriminator 标签的解析
discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);
} else {
// id 标签的解析
List<ResultFlag> flags = new ArrayList<ResultFlag>();
if ("id".equals(resultChild.getName())) {
flags.add(ResultFlag.ID);
}
// result标签的解析
resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));
}
}
// 通过ResultMapResolver将解析结果添加到Configuration的resultMaps注册表中
ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);
try {
return resultMapResolver.resolve();
} catch (IncompleteElementException e) {
configuration.addIncompleteResultMap(resultMapResolver);
throw e;
}
}
事实上,Mybatis中的<resultMap>
标签最后会变成一个ResultMap
对象。我们之前一共谈到了如下几个属性,他们与ResultMap
对象的属性对应关系如下:
- id:ResultMap.id
- type(Java类型):ResultMap.type
- result子标签集合:ResultMap.resultMappings
- id子标签:ResultMap.idResultMappings
- 构造器子标签:ResultMap.constructorResultMappings
- 鉴别器子标签:ResultMap.discriminator
- autoMapping属性:ResultMap.autoMapping
需要注意,各个XXXResultMappings属性都是ResultMapping对象列表
。ResultMapping
主要用来存储java对象属性与数据库字段之间的对应关系,例如:
<result property="doorCount" column="door_count" />
上面的xml配置就会生成一个ResultMapping对象。我们根据这一段xml文件配置查看一下ResultMapping
到底如何进行存储的?ResultMapping对象具有如下几个属性,分别代表的意义列在注释上:
// 保存该ResultMap的Configuration对象
private Configuration configuration;
// 当前标签所代表的属性,上面例子中则是doorCount
private String property;
// 当前标签所配置的数据库列名,上面例子中则是door_count
private String column;
// 当前字段在Java中的类型
private Class<?> javaType;
// 当前字段在数据库中的Jdbc类型
private JdbcType jdbcType;
// 处理该类型字段的类型处理器
private TypeHandler<?> typeHandler;
// 对于Mybatis来说经常会有如下的情况
// <collection property="posts" ofType="Post" resultMap="blogPostResult" columnPrefix="post_"/>
// 如果是这种情况,就会将resultMap的id填在此处
private String nestedResultMapId;
// 对于Mybatis的resultMap标签会有如下情况
// <collection property="posts" javaType="ArrayList" column="id" ofType="Post" select="selectPostsForBlog"/>
// 如果是这种情况,就会把select中指定的查询语句的id填在此处
private String nestedQueryId;
private Set<String> notNullColumns;
// 列前缀
private String columnPrefix;
// 当前字段的标识,例如该字段是ID
private List<ResultFlag> flags;
// 对于Mybatis的resultMap标签会有如下情况:
// <collection property="posts" ofType="domain.blog.Post">
// <id property="id" column="post_id"/>
// <result property="subject" column="post_subject"/>
// <result property="body" column="post_body"/>
// </collection>
// 这样就会出现ResultMapping的嵌套关系,而这里就用来存储这一嵌套关系
private List<ResultMapping> composites;
private String resultSet;
// 外键
private String foreignColumn;
// 该字段是否需要懒加载
private boolean lazy;
通过上面的解析,我们已经了解了<resultMap>
标签在Mybatis配置对象中保存的结构,其实每个<resultMap>
标签就是一个ResultMap
对象,这个对象中存储了对象属性与数据库字段的关系,并且存储了获取这些对象使用的查询sqlId,就好像一个命令模式,提供了输出格式和操作过程,由执行器执行具体的逻辑(SQL查询语句),然后拼接返回结果。
解析sql标签
在mapper
映射文件的配置中,我们经常用到如下配置:
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
这种引用某个SQL片段的情况。<sql>
标签就是为了防止重复书写相同配置而存在的。该标签由XMLMapperBuilder.sqlElement(List<XNode>)
方法进行解析。下面我们考察该方法:
private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {
for (XNode context : list) {
// 获取databaseId属性
String databaseId = context.getStringAttribute("databaseId");
// 获取id属性
String id = context.getStringAttribute("id");
// 将id拼接上命名空间,构建成完整的id
id = builderAssistant.applyCurrentNamespace(id, false);
// 如果当前正在使用的数据库ID与获取到的SQL上的ID相同的话
// 那么引用这个sql标签
// 否则不使用它
if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {
sqlFragments.put(id, context);
}
}
}
注意,此处的应用是将XNode节点
放入sqlFragments
属性中,就是我们之前卖关子的属性。真正解析该标签的过程是在真正解析操作数据库的sql语句的时候。话不多说,让我们考察那些SQL语句吧。
解析语句标签
真正操作数据库,执行数据库操作的SQL是由<insert>
、select
、update
、delete
标签声明的,这四个标签分别声明了插入、查找、更新、删除操作。事实上,对于mybatis来说,数据库操作只有两种:
- 查找:select
- 更新:包括insert、update、delete
处理上述四个标签解析逻辑的是XMLMapperBuilder.buildStatementFromContext(List<XNode>)
方法。该方法传入所有的数据库操作SQL标签的XML节点
,源码如下:
private void buildStatementFromContext(List<XNode> list, String requiredDatabaseId) {
// 遍历所有的insert、update、delete、select标签
for (XNode context : list) {
// 为每个标签创建XMLStatementBuilder
final XMLStatementBuilder statementParser = new XMLStatementBuilder(configuration, builderAssistant, context, requiredDatabaseId);
try {
// 通过XMLStatementBuilder进行解析
statementParser.parseStatementNode();
} catch (IncompleteElementException e) {
configuration.addIncompleteStatement(statementParser);
}
}
}
之前已经说过,在Mybatis的BaseBuilder类有多个子类,其中除了XMLConfigBuilder
类以外,其余的类都是用于解析<mapper>
标签的,这里的XMLStatementBuilder
就是用来解析<mapper>
标签中声明的持久化操作的。具体的解析逻辑在XMLStatementBuilder.parseStatementNode()
方法中。注意在创建XMLStatementBuilder
传入了如下参数:
- configuration:Mybatis配置存储的配置对象
- builderAssistant:配置解析的协助器,用于简化各种复杂的构建操作,并将构建的结果对象添加到configuration对象中
- context:要解析的XML节点
- requiredDatabaseId:mybatis配置中指定的databaseId,由于之前已经提到,
<sql>
标签只有在自己声明的databaseId
与Mybatis配置的databaseId
相同的情况下才会使用。而sql
标签内容的解析在各个使用了include
标签的地方进行解析。
明确了上面的内容之后,我们就可以开始分析解析操作数据的sql
的真正逻辑了,即XMLStatementBuilder.parseStatementNode()
方法,源码如下:
public void parseStatementNode() {
// 获取SQL语句ID
String id = context.getStringAttribute("id");
// 获取SQL语句适应的数据库
String databaseId = context.getStringAttribute("databaseId");
// 如果当前sql指定的数据库与Mybatis中配置的不同,则不进行解析
// 需要注意,这里还做了一个判重操作,如果有相同id的语句已经解析过,并且其databaseId不为null,那么就跳过本次解析
if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
return;
}
// 获取fetchSize属性
Integer fetchSize = context.getIntAttribute("fetchSize");
// 获取timeout属性
Integer timeout = context.getIntAttribute("timeout");
// 获取parameterMap属性
String parameterMap = context.getStringAttribute("parameterMap");
// 获取parameterType属性
String parameterType = context.getStringAttribute("parameterType");
// 将parameterType解析成Java的Class对象
Class<?> parameterTypeClass = resolveClass(parameterType);
// 获取resultMap属性
String resultMap = context.getStringAttribute("resultMap");
// 获取resultType属性
String resultType = context.getStringAttribute("resultType");
// 获取语言属性
String lang = context.getStringAttribute("lang");
// 查找对应的语言驱动
LanguageDriver langDriver = getLanguageDriver(lang);
// 将resultType属性解析为对应的Java的Class对象
Class<?> resultTypeClass = resolveClass(resultType);
String resultSetType = context.getStringAttribute("resultSetType");
StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);
String nodeName = context.getNode().getNodeName();
SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
// 判断SQL是否是查询语句
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());
// Parse selectKey after includes and remove them.
processSelectKeyNodes(id, parameterTypeClass, langDriver);
// 解析SQL与动态SQL标签
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
String resultSets = context.getStringAttribute("resultSets");
String keyProperty = context.getStringAttribute("keyProperty");
String keyColumn = context.getStringAttribute("keyColumn");
KeyGenerator keyGenerator;
String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
// 处理自动生成id
keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
if (configuration.hasKeyGenerator(keyStatementId)) {
keyGenerator = configuration.getKeyGenerator(keyStatementId);
} else {
keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
}
// 将解析结果构建成一个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);
}
其实上述解析操作大致可以分为5步:
- 判断
databaseId
与当前配置是否匹配,并判断该id的语句是否被解析过,这就是问什么要优先获取id的原因 - 获取标签基本信息
- 处理include标签
- 处理自动生成key的配置
- 解析SQL
- 构建MappedStatement放入到configuration对象中
其中第一步和第二步都比较简单,这里我们主要介绍后面三个步骤。首先是处理include标签。
include标签的处理
<include>
标签就是用来引用之前在sql
标签中声明的sql的。上面的代码中处理include
标签的是如下几行:
XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
includeParser.applyIncludes(context.getNode());
可以看到对于<include>
标签的解析交给了XMLIncludeTransformer
这一解析器,该解析器与别的解析器有一个很大的不同,就是别的解析器都是解析标签,然后将标签的内容封装为一个Java对象,放到Configuration对象中,而这个解析器则是,修改XML解析对象,然后再次交给XMLStatementsBuilder
进行解析。了解了这一点之后,我们可以考察一下XMLIncludeTransformer.applyIncludes(Node source, final Properties variablesContext, boolean included)
方法的源码:
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {
// 情况1
// 如果传入的节点是<include>节点
// 那么获取refid属性,以及<include>中声明的参数
// 根据这些去查找对应的sql段
// 如果sql段中有<include>标签,则递归处理
// 否则将include标签替换成sql标签
// 然后再将sql标签中所有的内容替换到sql标签所在的位置
if (source.getNodeName().equals("include")) {
Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);
Properties toIncludeContext = getVariablesContext(source, variablesContext);
applyIncludes(toInclude, toIncludeContext, true);
if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {
toInclude = source.getOwnerDocument().importNode(toInclude, true);
}
source.getParentNode().replaceChild(toInclude, source);
while (toInclude.hasChildNodes()) {
toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);
}
toInclude.getParentNode().removeChild(toInclude);
// 情况2
// 如果传入的标签是一般的非<include>标签
// 则应用属性表中的属性到标签中,然后递归初期传入标签的子标签
} else if (source.getNodeType() == Node.ELEMENT_NODE) {
if (included && !variablesContext.isEmpty()) {
// replace variables in attribute values
NamedNodeMap attributes = source.getAttributes();
for (int i = 0; i < attributes.getLength(); i++) {
Node attr = attributes.item(i);
attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext));
}
}
NodeList children = source.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
applyIncludes(children.item(i), variablesContext, included);
}
// 情况3
// 如果传入的标签是文本标签
// 那么应用属性表中的属性
} else if (included && source.getNodeType() == Node.TEXT_NODE
&& !variablesContext.isEmpty()) {
// replace variables in text node
source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));
}
}
处理<include>
标签使用了递归的方式进行处理。这里我们举例说明会比较形象,考虑下面的例子:
<sql id="sometable">
${prefix}Table
</sql>
<sql id="someinclude">
from
<include refid="${include_target}"/>
</sql>
<select id="select" resultType="map">
select
field1, field2, field3
<include refid="someinclude">
<property name="prefix" value="Some"/>
<property name="include_target" value="sometable"/>
</include>
</select>
一般情况下,我们解析id为select
的<select>
标签,这里包含了一个<include>
标签,实际上会将该<select>
标签传入到applyIncludes(Node source, final Properties variablesContext, boolean included)
,其中source
就是该<select>
标签内容,variablesContext
是<include>
标签中的属性表,最后的included
表示是不是include
标签内的内容。
这里首先会调用情况2,解析<select>
标签,获取所有该标签的子标签进行遍历处理<include>
标签,事实上,该标签只有两个标签,第一个是select field1, field2, field3
,第二个是include
标签,这里第一个标签交给情况3处理,<include>
标签交给情况1处理。
需要注意的是,可以发现,Mybatis并没有进行<sql>
标签自身的循环依赖问题处理。实际上如果出现了此问题,Mybatis只能是在解析时无限递归导致stackOverFlow
,并不会给出任何提示,不像是Spring还会给出出现了循环依赖问题。
例如,你写的sql
标签是这样的:
<sql id="id">
c.id
<include refid="id"></include>
</sql>
这种情况就会导致<include>
标签的解析出现无限的递归,因此,出现stackOverFlow
的情况。
selectKey标签的处理
Mybatis的<selectKey>
标签用于处理自动生成主键的问题。有些数据库或者JDBC驱动不支持自动生成主键,因此只能自己创建自定义主键。但是频繁调用setter方法设置主键又不好,因此就有了selectKey这一标签。Mybatis中给出的<selectKey>
标签的使用方法如下:
<insert id="insertAuthor">
<selectKey keyProperty="id" resultType="int" order="BEFORE">
select CAST(RANDOM()*1000000 as INTEGER) a from SYSIBM.SYSDUMMY1
</selectKey>
insert into Author
(id, username, password, email,bio, favourite_section)
values
(#{id}, #{username}, #{password}, #{email}, #{bio}, #{favouriteSection,jdbcType=VARCHAR})
</insert>
简单介绍一下<selectKey>
子标签的属性:
- keyProperty:selectKey 语句结果应该被设置到的目标属性。如果生成列不止一个,可以用逗号分隔多个属性名称。
- keyColumn:返回结果集中生成列属性的列名。如果生成列不止一个,可以用逗号分隔多个属性名称。
- resultType:结果的类型。通常 MyBatis 可以推断出来,但是为了更加准确,写上也不会有什么问题。MyBatis 允许将任何简单类型用作主键的类型,包括字符串。如果生成列不止一个,则可以使用包含期望属性的 Object 或 Map。
- order:可以设置为 BEFORE 或 AFTER。如果设置为 BEFORE,那么它首先会生成主键,设置 keyProperty 再执行插入语句。如果设置为 AFTER,那么先执行插入语句,然后是 selectKey 中的语句 - 这和 Oracle 数据库的行为相似,在插入语句内部可能有嵌入索引调用。
- statementType:和前面一样,MyBatis 支持 STATEMENT,PREPARED 和 CALLABLE 类型的映射语句,分别代表 Statement, PreparedStatement 和 CallableStatement 类型。
处理<selectKey>
标签的是XMLStatementBuilder.processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver)
方法。考察该方法源码如下:
private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
List<XNode> selectKeyNodes = context.evalNodes("selectKey");
// 处理<selectKey>标签
if (configuration.getDatabaseId() != null) {
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
}
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
// 移除<selectKey>标签
removeSelectKeyNodes(selectKeyNodes);
}
通过刚才给出的例子我们可以看到,<selectKey>
标签中的内容仅用于查询一个值,该值用于作为<insert>
操作的主键,因此真正执行插入操作时是不需要这条sql的,因此需要在解析之后把它清除掉。
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);
这两行代码负责解析<selectKey>
标签,而后面的removeSelectKeyNodes(selectKeyNodes)
负责将<selectKey>
标签移除。
下面我们考察解析操作中占主要地位的XMLStatementBuilder.parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId)
方法。该方法其实仅仅是读取所有的<selectKey>
标签然后进行解析,代码如下:
private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
// 遍历所有的`<selectKey>`标签,对每个标签进行解析
for (XNode nodeToHandle : list) {
String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
String databaseId = nodeToHandle.getStringAttribute("databaseId");
if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
}
}
}
可以看到,对于每个<selectKey>
标签来说,他的默认id是其父id+!selectKey
。例如上面的例子中,<selectKey>
的id就是insertAuthor!selectKey
。不过实际上<selectKey>
就是一个查询语句,也就相当于<select>
标签,Mybatis确实也是这样处理的。我们都知道对于<select>
标签来说,最后都会变成一个MappedStatement
。对于<selectKey>
标签也是,该MappedStatement
的id就是我们上面生成的那个id。真正构建MappedStatement
的方法就是parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
这行代码。也就是XMLStatementBuilder.parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId)
方法。源码如下 :
private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
// 获取返回类型
String resultType = nodeToHandle.getStringAttribute("resultType");
Class<?> resultTypeClass = resolveClass(resultType);
// 获取statementType
StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
// 获取Java属性名称
String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
// 获取数据库属性名称
String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
// 判断是在插入操作之前调用还是之后调用
boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));
//defaults
boolean useCache = false;
boolean resultOrdered = false;
KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
Integer fetchSize = null;
Integer timeout = null;
boolean flushCache = false;
String parameterMap = null;
String resultMap = null;
ResultSetType resultSetTypeEnum = null;
SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);
SqlCommandType sqlCommandType = SqlCommandType.SELECT;
// 创建一个MappedStatement保存该查询操作
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
id = builderAssistant.applyCurrentNamespace(id, false);
MappedStatement keyStatement = configuration.getMappedStatement(id, false);
// 使用该查询Sql创建一个KeyGenerator
// 并将该查询sql与keyGenerator简历联系
configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
事实证明,通常情况下,我们很少使用<selectKey>
标签显式指定id,更习惯的是使用数据库的自增id,这种时候,我们只需要在<insert>
的useGeneratedKeys
属性置为true。这时候使用的就是默认的Jdbc3KeyGenerator
。
至于各种KeyGenerator的具体执行流程,我们在后面讲解Mybatis执行流程时将会讨论该问题。
解析SQL
到这里其实我们已经快把Mybatis的配置解析讨论完了,但是我们并没有细节的讨论Mybatis的SQL到底是怎么解析的到底放在哪儿,我们只知道,保存SQL的就是一个叫SqlSource
的对象,这个对象到底是怎样生成的呢?事实上我们还有一个BaseBuilder没有讨论,就是XMLScriptBuilder
,其实该BaseBuilder
是用来做动态SQL的解析的,但是事实上也用来解析一般SQL。解析入口就是上面的如下方法:
public void parseStatementNode() {
...
LanguageDriver langDriver = getLanguageDriver(lang);
...
// 解析SQL与动态SQL标签
SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);
...
}
可以看到解析SQL和动态SQL标签的是一个LanguageDriver对象,该对象通过XMLStatementBuilder.getLanguageDriver(String lang)
方法处理,查看该方法:
private LanguageDriver getLanguageDriver(String lang) {
Class<? extends LanguageDriver> langClass = null;
if (lang != null) {
langClass = resolveClass(lang);
}
return configuration.getLanguageDriver(langClass);
}
事实上,很少有人去设置SQL语言,所以一般都用的默认配置,即XMLLanguageDriver
,一般我们获取到的就是这个,如果想要设置可以通过<settings>
标签的defaultScriptingLanguage
属性设置。让我们考察这个XMLLanguageDriver
的createSqlSource(Configuration configuration, XNode script, Class<?> parameterType)
方法,该方法负责创建SqlSource。源码如下:
public SqlSource createSqlSource(Configuration configuration, XNode script, Class<?> parameterType) {
XMLScriptBuilder builder = new XMLScriptBuilder(configuration, script, parameterType);
return builder.parseScriptNode();
}
可以看到,创建SqlSource的工作其实就是我们没有讨论的XMLScriptBuilder
做的。查看该类的parseScriptNode()
方法:
public SqlSource parseScriptNode() {
// 解析动态SQL标签
MixedSqlNode rootSqlNode = parseDynamicTags(context);
SqlSource sqlSource;
// 如果含有动态SQL标签则返回结果为DynamicSqlSource
// 否则返回RawSqlSource
if (isDynamic) {
sqlSource = new DynamicSqlSource(configuration, rootSqlNode);
} else {
sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType);
}
return sqlSource;
}
可以看到,如果传入的SQL有动态SQL标签,则返回的是DynamicSqlSource
,否则返回的是RawSqlSource
。事实上RawSqlSource
就是<select>
这类标签中写入的SQL,而DynamicSqlSource
则是一个SQLNode
列表,每一部分动态SQL就是一个SQLNode,这部分相对较为复杂,等到之后讲解真正解析SQL时会详细描述。
MappedStatement的结构
执行了上面的所有操作,我们就已经做好了充足的准备构建一个新的MappedStatement
用来表示一个操作的SQL语句
。那么MappedStatement到底是怎么和我们刚才获取到的数据对应上的呢?下面让我们考虑MappedStatement
的属性以及其业务含义。
// 该MappedStatement所对应的mapper.xml
private String resource;
// 存储该MappedStatement的Configuration对象
private Configuration configuration;
// 该MappedStatement的id
private String id;
// 该MappedStatement获取结果的默认长度
private Integer fetchSize;
// 该MappedStatement代表的SQL执行的超时时间
private Integer timeout;
// 该MappedStatement的Statement类型
private StatementType statementType;
// 该MappedStatement代表的SQL的返回集合类型
private ResultSetType resultSetType;
// 该MappedStatement的Sql元数据
private SqlSource sqlSource;
// 该MappedStatement所使用的缓存
private Cache cache;
// 该MappedStatement所使用的参数列表
private ParameterMap parameterMap;
// 该MappedStatement所使用的结果集列表
private List<ResultMap> resultMaps;
// 执行该MappedStatement所代表的操作时是否要刷新缓存
private boolean flushCacheRequired;
// 执行该MappedStatement所代表的操作时是否要使用缓存
private boolean useCache;
private boolean resultOrdered;
// 该MappedStatement所代表SQL的类型(UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;)
private SqlCommandType sqlCommandType;
// 该MappedStatement的对应key生成器
private KeyGenerator keyGenerator;
// 该MappedStatement对应的key生成器使用的属性
private String[] keyProperties;
// 该MappedStatement对应的key生成器对应的数据库列
private String[] keyColumns;
private boolean hasNestedResultMaps;
// 数据库类型
private String databaseId;
// statementLog
private Log statementLog;
private LanguageDriver lang;
private String[] resultSets;
可以看到MappedStatement包含了一条SQL执行的所有格式上的数据,只缺少查询参数这类内容数据,这就像一个命令模式,只需要传入命令格式和对应的内容就可以执行。
这里除了对MappedStatement
结构进行讲解以外,还要特别讲解一下我们的如下方法:
builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
resultSetTypeEnum, flushCache, useCache, resultOrdered,
keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);
在我们分析了那么多的Mybatis配置解析源码都没有仔细提这个builderAssistant
,即我们的构建协助器
,但是现在我们不得不说一下该类的addMappedStatement(XXXX)
方法,因为这涉及到我们的Mapper
到底使用哪个缓存,下面我们考察这个方法的部分代码:
public MappedStatement addMappedStatement(...) {
...
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 statementParameterMap = getStatementParameterMap(parameterMap, parameterType, id);
if (statementParameterMap != null) {
statementBuilder.parameterMap(statementParameterMap);
}
MappedStatement statement = statementBuilder.build();
configuration.addMappedStatement(statement);
return statement;
}
可以看到,最后Mapper真正使用的缓存就是这个currentCache。更改这个属性的代码就在解析<cache>
和<cache-ref>
标签中。
了解了最主要的MappedStatement
,我们其实就可以开始分析Mybatis的执行流程了。但是,MappedStatement
到底是怎么和对应的Class对象联系起来的呢 ?
标识资源已经解析
让我们现在再回归XMLMapperBuilder
的parse()
方法上,再次给出该方法源码:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
我们之前讨论的那么多都是configurationElement(parser.evalNode("/mapper"));
这一行代码。接下来我们会继续讨论接下来的代码。这里我们讨论的是configuration.addLoadedResource(resource);
这行代码负责将已解析的资源添加到一个注册表中(Configuration的loadedResources
属性中),避免重复解析。这一点功能很明确。但是,除此之外还有另一个功能,实际上Mybatis并不是所有的配置都是XML文件书写的,有一部分配置是在Java类上的。因此这部分配置也要进行解析。但是解析之后可能有XML配置与Java配置两者互补的地方。因此就需要避免重复解析,直接调用后面的三个parseXXX
方法就好了。而刚才说到的解析不全的地方,相比通过阅读上面一节已经很清楚了,就被保存在Configuration
对象的incompleteXXX
对象中。
将XML文件绑定到对应的Java Class对象上
最后让我们讲解一下XML文件是如何和Java对象结合的,使用过Mybatis的读者都知道,mapper.xml
的namespace
属性的值就是Java类的权限定类名,那这一步对应是怎么做的呢?
我们仍然查看XMLMapperBuilder
的parse()
方法上,方法源码如下:
public void parse() {
if (!configuration.isResourceLoaded(resource)) {
configurationElement(parser.evalNode("/mapper"));
configuration.addLoadedResource(resource);
bindMapperForNamespace();
}
parsePendingResultMaps();
parsePendingCacheRefs();
parsePendingStatements();
}
请注意bindMapperForNamespace();
这行代码,方法名已经很明确了,这就是将Mapper Class对象和命名空间整合的地方,让我们考察该方法的源码:
private void bindMapperForNamespace() {
// 获取当前的命名空间
String namespace = builderAssistant.getCurrentNamespace();
if (namespace != null) {
Class<?> boundType = null;
try {
// 通过命名空间获取对应的Class对象
boundType = Resources.classForName(namespace);
} catch (ClassNotFoundException e) {
//ignore, bound type is not required
}
// 如果获取到的Class对象不为空
// 查找有没有为其分配MappedStatement
// 如果没有则添加解析记录
// 并且将Class对象与XML文件联系起来(即将Class对象添加到Configuration的knownMappers注册表中)
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);
}
}
}
}
通过上面的代码可以看到,将MappedStatement联系到Class对象,仅仅是确保Class对象已经解析过,然后将Class对象放入到Configuration的knownMappers注册表中。如果没解析过得话,那就开始解析Java的Class对象,可以考察configuration.addMapper(boundType);
方法:
public <T> void addMapper(Class<T> type) {
mapperRegistry.addMapper(type);
}
可以看到,这就是接下来要讲解的基于Java注解配置的Mapper对象的解析方法。所以在解析XML文件配置后紧接着就会解析Java的Class对象。除此之外,上述代码还有如下部分需要注意:
// 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);
说实话,个人认为,这是一个极其智障的注释,但是是一个良好的设计,我们知道Mybatis的每一个Mapper对应一个命名空间,由于Mybatis防止资源重复解析是通过Configuration的loadedResources
注册表完成的,该注册表是一个String的Set,如果该注册表存入的是资源名称,那么就会出现很大的问题。命名空间就将具体的文件与配置进行了解耦。
既然已经提到了基于注解的Mapper标签解,那么让我们进入下一节:《7.Java注解方式配置的mapper标签的解析》。