转载自 mybatis源码阅读(三):mybatis初始化(下)mapper解析
MyBatis 的真正强大在于它的映射语句,也是它的魔力所在。由于它的异常强大,映射器的 XML 文件就显得相对简单。如果拿它跟具有相同功能的 JDBC 代码进行对比,你会立即发现省掉了将近 95% 的代码。MyBatis 就是针对 SQL 构建的,并且比普通的方法做的更好。
SQL 映射文件有很少的几个顶级元素(按照它们应该被定义的顺序):
- cache – 给定命名空间的缓存配置。
- cache-ref – 其他命名空间缓存配置的引用。
- resultMap – 是最复杂也是最强大的元素,用来描述如何从数据库结果集中来加载对象。
parameterMap– 已废弃!老式风格的参数映射。内联参数是首选,这个元素可能在将来被移除,这里不会记录。- sql – 可被其他语句引用的可重用语句块。
- insert – 映射插入语句
- update – 映射更新语句
- delete – 映射删除语句
- select – 映射查询语句
对每个标签的属性以及作用,这里不做解释, 可以参考官方文档:http://www.mybatis.org/mybatis-3/zh/sqlmap-xml.html
上一篇文章介绍了mybatis配置文件解析mappers节点的源码中有如下语句,从这里得到mapper映射文件时通过XMLMapperBuilder解析的。
一、XMLMapperBuilder
//mapper映射文件都是通过XMLMapperBuilder解析XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());mapperParser.parse();
//解析mapper文件
public void parse() {// 判断是否已经加载过改映射文件if (!configuration.isResourceLoaded(resource)) {// 处理mapper节点configurationElement(parser.evalNode("/mapper"));// 将resource添加到configuration的loadedResources集合中保存 它是HashSet<String>configuration.addLoadedResource(resource);//注册mapper接口bindMapperForNamespace();}// 处理解析失败的resultMap节点parsePendingResultMaps();// 处理解析失败的cache-ref节点parsePendingCacheRefs();// 处理解析失败的sql节点parsePendingStatements();
}
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);// 解析cache-ref节点cacheRefElement(context.evalNode("cache-ref"));// 解析cache节点cacheElement(context.evalNode("cache"));// 解析parameterMap节点,这个已经被废弃,不推荐使用parameterMapElement(context.evalNodes("/mapper/parameterMap"));// 解析resultMap节点resultMapElements(context.evalNodes("/mapper/resultMap"));// 解析sql节点sqlElement(context.evalNodes("/mapper/sql"));// 解析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);}
}
1.cache节点
它通过调用CacheBuilder的相应方法完成cache的创建。每个cache内部都有一个唯一的ID,这个id的值就是namespace。创建好的cache对象存入configuration的cache缓存中(该缓存以cache的ID属性即namespace为key,这里再次体现了mybatis的namespace的强大用处)。
/*** cache- 配置本定命名空间的缓存。* type- cache实现类,默认为PERPETUAL,可以使用自定义的cache实现类(别名或完整类名皆可)* eviction- 回收算法,默认为LRU,可选的算法有:* LRU– 最近最少使用的:移除最长时间不被使用的对象。* FIFO– 先进先出:按对象进入缓存的顺序来移除它们。* SOFT– 软引用:移除基于垃圾回收器状态和软引用规则的对象。* WEAK– 弱引用:更积极地移除基于垃圾收集器状态和弱引用规则的对象。* flushInterval- 刷新间隔,默认为1个小时,单位毫秒* size- 缓存大小,默认大小1024,单位为引用数* readOnly- 只读* @param context* @throws Exception*/
private void cacheElement(XNode context) throws Exception {if (context != null) {String type = context.getStringAttribute("type", "PERPETUAL");Class<? extends Cache> typeClass = typeAliasRegistry.resolveAlias(type);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 props = context.getChildrenAsProperties();builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);}
}
public Cache useNewCache(Class<? extends Cache> typeClass,Class<? extends Cache> evictionClass,Long flushInterval,Integer size,boolean readWrite,boolean blocking,Properties props) {Cache cache = new CacheBuilder(currentNamespace).implementation(valueOrDefault(typeClass, PerpetualCache.class)).addDecorator(valueOrDefault(evictionClass, LruCache.class)).clearInterval(flushInterval).size(size).readWrite(readWrite).blocking(blocking).properties(props).build();configuration.addCache(cache);currentCache = cache;return cache;
}
2.cache-ref节点
cacheRefElement方法负责解析cache-ref元素,它通过调用CacheRefResolver的相应方法完成cache的引用。创建好的cache-ref引用关系存入configuration的cacheRefMap缓存中。
/*** cache-ref–从其他命名空间引用缓存配置。* 如果你不想定义自己的cache,可以使用cache-ref引用别的cache。* 因为每个cache都以namespace为id,* 所以cache-ref只需要配置一个namespace属性就可以了。* 需要注意的是,如果cache-ref和cache都配置了,以cache为准。* @param context*/
private void cacheRefElement(XNode context) {if (context != null) {configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace"));CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace"));try {cacheRefResolver.resolveCacheRef();} catch (IncompleteElementException e) {configuration.addIncompleteCacheRef(cacheRefResolver);}}
}
3.resultMap节点
resultMapElement方法负责解析resultMap元素,它通过调用ResultMapResolver的相应方法完成resultMap的解析。resultMap节点下除了discriminator子节点的其他子节点都会解析成对应的ResultMapping对象,而每个<resultMap>节点都会被解析成一个ResultMap对象,创建好的resultMap存入configuration的resultMaps缓存中(该缓存以namespace+resultMap的id为key,这里再次体现了mybatis的namespace的强大用处)。
private void resultMapElements(List<XNode> list) throws Exception {for (XNode resultMapNode : list) {try {resultMapElement(resultMapNode);} catch (IncompleteElementException e) {// ignore, it will be retried}}
}private ResultMap resultMapElement(XNode resultMapNode) throws Exception {return resultMapElement(resultMapNode, Collections.<ResultMapping> emptyList());
}private ResultMap resultMapElement(XNode resultMapNode, List<ResultMapping> additionalResultMappings) throws Exception {ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier());String id = resultMapNode.getStringAttribute("id",resultMapNode.getValueBasedIdentifier());String type = resultMapNode.getStringAttribute("type",resultMapNode.getStringAttribute("ofType",resultMapNode.getStringAttribute("resultType",resultMapNode.getStringAttribute("javaType"))));String extend = resultMapNode.getStringAttribute("extends");Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping");Class<?> typeClass = resolveClass(type);Discriminator discriminator = null;List<ResultMapping> resultMappings = new ArrayList<ResultMapping>();resultMappings.addAll(additionalResultMappings);List<XNode> resultChildren = resultMapNode.getChildren();for (XNode resultChild : resultChildren) {if ("constructor".equals(resultChild.getName())) {processConstructorElement(resultChild, typeClass, resultMappings);} else if ("discriminator".equals(resultChild.getName())) {discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings);} else {List<ResultFlag> flags = new ArrayList<ResultFlag>();if ("id".equals(resultChild.getName())) {flags.add(ResultFlag.ID);}resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags));}}ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping);try {return resultMapResolver.resolve();} catch (IncompleteElementException e) {configuration.addIncompleteResultMap(resultMapResolver);throw e;}
}
4.sql节点解析
sql节点用来定义可重用的sql语句片段, sqlElement方法负责解析sql元素。id属性用于区分不同的sql元素,在同一个mapper配置文件中可以配置多个sql元素。
private void sqlElement(List<XNode> list) throws Exception {if (configuration.getDatabaseId() != null) {sqlElement(list, configuration.getDatabaseId());}sqlElement(list, null);
}private void sqlElement(List<XNode> list, String requiredDatabaseId) throws Exception {for (XNode context : list) {String databaseId = context.getStringAttribute("databaseId");String id = context.getStringAttribute("id");id = builderAssistant.applyCurrentNamespace(id, false);if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) {// 记录到sqlFragments中保存,其实 构造函数中可以看到该字段指向了configuration的sqlFragments集合中sqlFragments.put(id, context);}}
}private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {if (requiredDatabaseId != null) {if (!requiredDatabaseId.equals(databaseId)) {return false;}} else {if (databaseId != null) {return false;}// skip this fragment if there is a previous one with a not null databaseIdif (this.sqlFragments.containsKey(id)) {XNode context = this.sqlFragments.get(id);if (context.getStringAttribute("databaseId") != null) {return false;}}}return true;
}
二、XMLStatementBuilder
映射配置文件中还有一类比较重要的节点需要解析,其实就是select|insert|update|delete 节点,这些节点主要用于定义SQL语句,他们不在由XMLMapperBuilder进行解析,而是由XMLStatementBuilder负责进行解析,每个节点会被解析成MappedStatement对象并存入到configuration对象中去。在这个方法内有几个重要的步骤,理解他们对正确的配置statement元素很有帮助。
1.MappedStatement
MappedStatement包含了这些节点的很多属性,其中比较重要的如下:
private String resource;//节点中的id 包括命名空间
private SqlSource sqlSource;//SqlSource对象,对应一条SQL语句
private SqlCommandType sqlCommandType;//SQL的类型,insert,delete,select,update
解析过程代码如下:
public void parseStatementNode() {// 获取sql节点的id以及databaseId如果和当前不匹配不加载改节点,// 如果存在id相同且databaseId不为空的节点也不在加载改节点String id = context.getStringAttribute("id");String databaseId = context.getStringAttribute("databaseId");if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {return;}// 获取节点的多种属性Integer fetchSize = context.getIntAttribute("fetchSize");Integer timeout = context.getIntAttribute("timeout");String parameterMap = context.getStringAttribute("parameterMap");String parameterType = context.getStringAttribute("parameterType");Class<?> parameterTypeClass = resolveClass(parameterType);String resultMap = context.getStringAttribute("resultMap");String resultType = context.getStringAttribute("resultType");String lang = context.getStringAttribute("lang");LanguageDriver langDriver = getLanguageDriver(lang);Class<?> resultTypeClass = resolveClass(resultType);String resultSetType = context.getStringAttribute("resultSetType");StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);// 根据节点的名称设置sqlCommandType的类型String nodeName = context.getNode().getNodeName();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);// 在解析SQL语句之前先处理include节点XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);includeParser.applyIncludes(context.getNode());// 处理selectKey节点processSelectKeyNodes(id, parameterTypeClass, langDriver);// 完成节点的解析 该部分是核心SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);// 获取resultSets keyProperty keyColumn三个属性String resultSets = context.getStringAttribute("resultSets");String keyProperty = context.getStringAttribute("keyProperty");String keyColumn = context.getStringAttribute("keyColumn");KeyGenerator keyGenerator;// 获取selectKey节点对应的selectKeyGenerator的idString keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;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;}// 通过MapperBuilderAssistant创建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);
}
2、解析include节点
在解析statement节点之前首先通过XMLIncludeTransformer解析include节点改过程会将include节点替换<sql>节点中定义的sql片段,并将其中的${xx}占位符换成真实的参数,
private void applyIncludes(Node source, final Properties variablesContext, boolean included) {if (source.getNodeName().equals("include")) { // ---(2)处理include节点// 查找refid属性指向的<sql>,返回的是深克隆的Node对象Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext);Properties toIncludeContext = getVariablesContext(source, variablesContext);//递归处理include节点applyIncludes(toInclude, toIncludeContext, true);if (toInclude.getOwnerDocument() != source.getOwnerDocument()) {toInclude = source.getOwnerDocument().importNode(toInclude, true);}// 将<include>节点替换<sql>节点source.getParentNode().replaceChild(toInclude, source);while (toInclude.hasChildNodes()) {// 将<sql>节点的子节点添加到<sql>节点的前面toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude);}// 替换后删除<sql>节点toInclude.getParentNode().removeChild(toInclude);} else if (source.getNodeType() == Node.ELEMENT_NODE) {// ---(1)if (included && !variablesContext.isEmpty()) {// replace variables in attribute valuesNamedNodeMap attributes = source.getAttributes();for (int i = 0; i < attributes.getLength(); i++) {// 遍历当前sql的子节点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);}} else if (included && source.getNodeType() == Node.TEXT_NODE&& !variablesContext.isEmpty()) {// ---(3)// replace variables in text node 替换对应的占位符source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext));}
}
3、解析selectKey节点
在insert,update节点中可以定义selectKey节点来解决主键自增问题。
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.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));String keyProperty = nodeToHandle.getStringAttribute("keyProperty");String keyColumn = nodeToHandle.getStringAttribute("keyColumn");boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));//defaultsboolean 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;// 生成SqlSourceSqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);// selectKey节点中只能配置select语句SqlCommandType sqlCommandType = SqlCommandType.SELECT;// 创建MappedStatement对象,并添加到configuration的mappedStatements集合中保存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);// 创建对应的KeyGenerator(主键自增策略),添加到configuration中configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
}
三、绑定Mapper接口
每个映射配置文件的命名空间可以绑定一个Mapper接口,并注册到MapperRegistry中。
// 绑定mapper接口
private void bindMapperForNamespace() {//获取映射文件的命名空间String namespace = builderAssistant.getCurrentNamespace();if (namespace != null) {Class<?> boundType = null;try {// 解析命名空间对应的类型 即daoboundType = 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);}}}
}