Mybatis-3 源码之缓存是怎么创建和使用的

InfoQ 2020-11-06 01:15:46
mybatis-3 mybatis 源码 缓存 创建


{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"Mybatis-3 源码之缓存是怎么创建的"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Mybatis 缓存问题其实也是面试高频的问题了,今天我们就从源码级别来谈谈 Mybatis 的缓存实现。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"(本文源码均在 https:\/\/github.com\/ccqctljx\/Mybatis-3 中,会持续更新注释和 Demo)。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们了解一下缓存是什么:缓存是一般的 ORM 框架都会提供的功能,目的就是提升查询的效率和减少数据库的压力。直白一点就是,开了缓存后,同样的数据查询不必再次访问数据库,直接从缓存中拿即可。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么面试官常问的 一级缓存 和 二级缓存 又都是什么呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"一级缓存:一级缓存又称本地缓存,是在会话(SqlSession)层面进行的缓存。随会话开始而生,结束而死。MyBatis 的一级缓存是默认开启的,不需要任何的配置。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"二级缓存:由于一级缓存随会话而生,就不能跨会话共享。二级缓存则是用来解决这个问题的,他的范围是 namespace 级别的,可以被多个SqlSession 共享,生命周期和 SqlSessionFactory 同步。只要是同一个 SqlSessionFactory 创建出来的会话,即可共享相同 namespace 级别的缓存。二级缓存需要配置三个地方:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一个是在 mybaits-config.xml 配置文件中设置开启缓存:``"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"``"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二个是要在 Mapper 文件中配置 ``"},{"type":"codeinline","content":[{"type":"text","text":" "}]},{"type":"text","text":"``标签"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第三个是在需要使用缓存的语句上加入 ``"},{"type":"codeinline","content":[{"type":"text","text":"useCache=\"true\" "}]},{"type":"text","text":"``"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么一级二级缓存有没有执行顺序什么的呢?答案是有的,如果开启二级缓存那么执行顺序为:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/resource\/image\/32\/52\/3208f20742e6fca73ddeba0934e4d852.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"那么我们写个实例代码,来看下一二级缓存的效果吧"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public class Demo {\n public static void main(String[] args) throws IOException {\n\n String resource = \"mybatis\/mybatis-config.xml\";\n InputStream inputStream = Resources.getResourceAsStream(resource);\n SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);\n SqlSession sqlSession1 = sqlSessionFactory.openSession();\n SqlSession sqlSession2 = sqlSessionFactory.openSession();\n\n List bookInfoList1 = sqlSession1.selectList(\"com.simon.demo.TestMapper.selectBookInfo\");\n System.out.println(\" sqlSession 1 query 1 ----------------------------- \" + bookInfoList1);\n\n List bookInfoList2 = sqlSession1.selectList(\"com.simon.demo.TestMapper.selectBookInfo\");\n System.out.println(\"sqlSession 1 query 2 -----------------------------\" + bookInfoList2);\n\n sqlSession1.commit();\n System.out.println(\"sqlSession 1 commit -----------------------------\");\n\n List bookInfoList3 = sqlSession2.selectList(\"com.simon.demo.TestMapper.selectBookInfo\");\n System.out.println(\"sqlSession 2 query 1 ----------------------------- \" + bookInfoList3);\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"打印结果是:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/resource\/image\/e1\/12\/e12a6b5b5f6e1fa753af2b06e1fac612.png","alt":null,"title":"","style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由此我们能看到,只有第一次查询执行了 sql,其余两次查询均未去数据库中查询。这就是缓存的效用啦。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们接下来去到源码来看一下究竟是如何生效的吧。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二级缓存创建过程一:加载配置类"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先,我们创建 SqlSessionFactory 工厂时,会从配置文件中加载所有的配置并生成 Configuration 对象,然后将 Configuration 对象放在 SqlSessionFactory 实例对象中维护起来。解析代码如下"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"package org.apache.ibatis.builder.xml;\npublic class XMLConfigBuilder extends BaseBuilder {\n ……\n private void parseConfiguration(XNode root) {\n try {\n \/\/issue #117 read properties first\n propertiesElement(root.evalNode(\"properties\"));\n\n \/\/ 解析配置文件里的 setting 标签\n Properties settings = settingsAsProperties(root.evalNode(\"settings\"));\n loadCustomVfs(settings);\n loadCustomLogImpl(settings);\n \/\/ 生成别名 map 放进 configuration 中后备使用\n typeAliasesElement(root.evalNode(\"typeAliases\"));\n pluginElement(root.evalNode(\"plugins\"));\n objectFactoryElement(root.evalNode(\"objectFactory\"));\n objectWrapperFactoryElement(root.evalNode(\"objectWrapperFactory\"));\n reflectorFactoryElement(root.evalNode(\"reflectorFactory\"));\n settingsElement(settings);\n \/\/ read it after objectFactory and objectWrapperFactory issue #631\n environmentsElement(root.evalNode(\"environments\"));\n databaseIdProviderElement(root.evalNode(\"databaseIdProvider\"));\n typeHandlerElement(root.evalNode(\"typeHandlers\"));\n\n \/\/ 解析配置文件里的 mappers 标签\n mapperElement(root.evalNode(\"mappers\"));\n } catch (Exception e) {\n throw new BuilderException(\"Error parsing SQL Mapper Configuration. Cause: \" + e, e);\n }\n }\n \n \/**\n * 把 settings 标签的所有配置加载成 Properties\n * @param context\n * @return\n *\/\n private Properties settingsAsProperties(XNode context) {\n if (context == null) {\n return new Properties();\n }\n Properties props = context.getChildrenAsProperties();\n \/\/ Check that all settings are known to the configuration class\n MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory);\n for (Object key : props.keySet()) {\n if (!metaConfig.hasSetter(String.valueOf(key))) {\n throw new BuilderException(\"The setting \" + key + \" is not known. Make sure you spelled it correctly (case sensitive).\");\n }\n }\n return props;\n }\n \n \/**\n * 设置全局上下文属性\n *\/\n private void settingsElement(Properties props) {\n ……\n configuration.setCacheEnabled(booleanValueOf(props.getProperty(\"cacheEnabled\"), true));\n configuration.setLocalCacheScope(LocalCacheScope.valueOf(props.getProperty(\"localCacheScope\", \"SESSION\")));\n ……\n }\n ……\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"方法 settingsAsProperties 将配置文件中 setting 标签读为 Properties 对象,然后在 settingsElement 方法中全部赋给 configuration 对象,这其中就有对 cache 标签的处理,将 。这个 Configuration 是 BaseBuilder 中描述全局配置的一个类,后面会将它扔给 SqlSessionFactory ,作为全局上下文。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里还有个方法比较重要,就是 typeAliasesElement 方法,这个方法是将我们配置好的一些别名类,以键值对的形式存储在 TypeAliasRegistry 类中的一个 HashMap 中,例如 \"byte\" -> Byte.class。这个 TypeAliasRegistry 也会被放入全局配置 Configuration 中。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二级缓存创建过程二:创建 Cache 对象并绑定 Mapper"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解析配置文件后,mybatis 知道自己需要开启二级缓存,于是开始了创建缓存之路,首先,先扫描所有 Mapper 文件位置,然后一个个分析过去(此处以 resource 为例分析):"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"package org.apache.ibatis.builder.xml;\npublic class XMLConfigBuilder extends BaseBuilder {\n private void mapperElement(XNode parent) throws Exception {\n if (parent != null) {\n \/\/ 遍历 mybatis-config.xml 文件下面的 mappers 节点的子节点\n for (XNode child : parent.getChildren()) {\n \/\/ 判断是否是 Package,如果是的话可以直接拿 Package 去加载包下的 mapper 文件\n if (\"package\".equals(child.getName())) {\n String mapperPackage = child.getStringAttribute(\"name\");\n configuration.addMappers(mapperPackage);\n } else {\n \/\/ 如果不是的话,就是 mapper 标签(因为 xml 中只允许写这两种标签)\n \/\/ 然后拿相应的属性,去分别作解析\n String resource = child.getStringAttribute(\"resource\");\n String url = child.getStringAttribute(\"url\");\n String mapperClass = child.getStringAttribute(\"class\");\n\n \/\/ 解析 resource 表明位置的 mapper\n if (resource != null && url == null && mapperClass == null) {\n \/\/ 此处定义错误上下文,如果这里加载出错日志打印 (\"### The error may exist in xxx\");\n ErrorContext.instance().resource(resource);\n \/\/ 读取 配置文件 成流\n InputStream inputStream = Resources.getResourceAsStream(resource);\n XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments());\n \/\/ 解析具体的 mapper 文件\n mapperParser.parse();\n }\n\n \/\/ 解析 url 表明位置的 mapper\n else if (resource == null && url != null && mapperClass == null) {\n ErrorContext.instance().resource(url);\n InputStream inputStream = Resources.getUrlAsStream(url);\n XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments());\n mapperParser.parse();\n }\n\n \/\/ 解析 mapperClass 表明位置的 mapper\n else if (resource == null && url == null && mapperClass != null) {\n Class mapperInterface = Resources.classForName(mapperClass);\n configuration.addMapper(mapperInterface);\n } else {\n throw new BuilderException(\"A mapper element may only specify a url, resource or class, but not more than one.\");\n }\n }\n }\n }\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"找到 Mapper 后,开始针对 Mapper 的解析:"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"package org.apache.ibatis.builder.xml;\npublic class XMLMapperBuilder extends BaseBuilder {\n ……\n public void parse() {\n \/\/ 因为是公共方法,多处调用,所以这里先判断有没有加载过\n if (!configuration.isResourceLoaded(resource)) {\n \/\/ 没加载过的话,先去加载资源,这里创建了 Cache 对象\n configurationElement(parser.evalNode(\"\/mapper\"));\n configuration.addLoadedResource(resource);\n bindMapperForNamespace();\n }\n\n parsePendingResultMaps();\n parsePendingCacheRefs();\n parsePendingStatements();\n }\n \n private void configurationElement(XNode context) {\n try {\n String namespace = context.getStringAttribute(\"namespace\");\n if (namespace == null || namespace.isEmpty()) {\n throw new BuilderException(\"Mapper's namespace cannot be empty\");\n }\n builderAssistant.setCurrentNamespace(namespace);\n \/\/ 这两行是开启二级缓存比较关键的两步\n \/\/ 这一步拿了别人的 cache 对象 设置给自己了\n cacheRefElement(context.evalNode(\"cache-ref\"));\n \/\/ 在这一步中构建了 Cache 对象\n cacheElement(context.evalNode(\"cache\"));\n \/\/ 解析参数 Map\n parameterMapElement(context.evalNodes(\"\/mapper\/parameterMap\"));\n \/\/ 解析 resultMap\n resultMapElements(context.evalNodes(\"\/mapper\/resultMap\"));\n \/\/ 解析每个 sql 标签(mapper 中有两种 sql,一种是 下面要解析的四打标签,还有直接用 sql 标签的)\n sqlElement(context.evalNodes(\"\/mapper\/sql\"));\n \/\/ 解析四大标签,并放入 configuration 中,这里也会为每个开启缓存的 statement 设置上面生成好的缓存对象,也就是 Cache\n buildStatementFromContext(context.evalNodes(\"select|insert|update|delete\"));\n } catch (Exception e) {\n throw new BuilderException(\"Error parsing Mapper XML. The XML location is '\" + resource + \"'. Cause: \" + e, e);\n }\n }\n ……\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这里我们跟缓存相关的有三步,第一步 cacheRefElement 是看看 mapper 中是否标注了 ``"},{"type":"codeinline","content":[{"type":"text","text":""}]},{"type":"text","text":"`"},{"type":"codeinline","content":[{"type":"text","text":" 标签,这个标签的意思是 我可以跟其他 namespace 的 mapper 共用一个 Cache。源码其实就是把 Configuration 中加载好的指定 mapper 的 Cache 对象引用给自己。我们重点看创建 Cache 对象的方法也就是 "}]},{"type":"text","text":"`"},{"type":"codeinline","content":[{"type":"text","text":"cacheElement(context.evalNode(\"cache\"));"}]},{"type":"text","text":"``"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"private void cacheElement(XNode context) {\n if (context != null) {\n \/\/ 如果不指定类型,则默认缓存类型设置为 PERPETUAL\n String type = context.getStringAttribute(\"type\", \"PERPETUAL\");\n \/\/ typeAliasRegistry 内部维护了一个 HashMap 并且预设了很多类别名,例如 \"byte\" -> Byte.class\n \/\/ 这里指的就是之前加载配置时 typeAliasesElement 方法所做的\n Class typeClass = typeAliasRegistry.resolveAlias(type);\n \/\/ eviction 意为驱逐、赶出。这里则代表着 缓存清除策略,即如何清除无用的缓存\n \/\/ 代码可以看到,默认是 LRU 即 移除最长时间不被使用的对象。\n \/\/ 官网文档共设有四种如下:\n \/**\n LRU – Least Recently Used: Removes objects that haven't been used for the longst period of time.(清除长时间不用的)\n FIFO – First In First Out: Removes objects in the order that they entered the cache.(清除最开始放进去的)\n SOFT – Soft Reference: Removes objects based on the garbage collector state and the rules of Soft References.(软引用式清除)\n WEAK – Weak Reference: More aggressively removes objects based on the garbage collector state and rules of Weak References.(弱引用式清除)\n *\/\n String eviction = context.getStringAttribute(\"eviction\", \"LRU\");\n Class evictionClass = typeAliasRegistry.resolveAlias(eviction);\n \/\/ 刷新间隔,单位 毫秒,代表一个合理的毫秒形式的时间段。默认情况是不设置,也就是没有刷新间隔,缓存仅仅调用 update 语句时刷新。\n Long flushInterval = context.getLongAttribute(\"flushInterval\");\n \/\/ 引用数目,要记住你缓存的对象数目和你运行环境的可用内存资源数目。默认值是1024。\n Integer size = context.getIntAttribute(\"size\");\n\n \/\/ 下面是针对缓存对象实例是否只读的配置\n \/\/ 只读的缓存会给所有调用者返回缓存对象的相同实例。因此这些对象不能被修改(一旦修改,别人取到的也是修改后的)。这提供了很重要的性能优势。\n \/\/ 可读写的缓存会返回缓存对象的拷贝(通过序列化)。这会慢一些,但是安全,因此默认是false。\n boolean readWrite = !context.getBooleanAttribute(\"readOnly\", false);\n \/\/ 设置是否是阻塞缓存,如果是 true ,则在创建缓存的时候会包装一层 BlockingCache 。默认为 false\n boolean blocking = context.getBooleanAttribute(\"blocking\", false);\n Properties props = context.getChildrenAsProperties();\n \/\/ 此方法构建了一个新的 Cache 对象并设置到了 configuration 中。\n builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props);\n }\n \n public Cache useNewCache(Class typeClass,\n Class evictionClass,\n Long flushInterval,\n Integer size,\n boolean readWrite,\n boolean blocking,\n Properties props) {\n \/\/ 此处使用建造者模式创建了 Cache,并且绑定了当前 Mapper 的命名空间并作为此 Cache 的 ID。\n Cache cache = new CacheBuilder(currentNamespace)\n \/\/ 缓存实现类\n .implementation(valueOrDefault(typeClass, PerpetualCache.class))\n \/\/ 包装类(缓存回收策略类)\n .addDecorator(valueOrDefault(evictionClass, LruCache.class))\n \/\/ 清除时间\n .clearInterval(flushInterval)\n .size(size)\n .readWrite(readWrite)\n .blocking(blocking)\n .properties(props)\n .build();\n \/\/ 构建好Cache后,加入到 configuration 中等待调用。\n configuration.addCache(cache);\n currentCache = cache;\n return cache;\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"创建完毕后,这里调用了 ``"},{"type":"codeinline","content":[{"type":"text","text":"configuration.addCache(cache)"}]},{"type":"text","text":"`` 方法将生成好的 cache 放进了 configuration 对象中,实际上就是将 cache 对象 put 进了 Configuration 类内部维护的一个 StrictMap中,而这个 StrictMap 则是继承自 HashMap, 也就是说归根结底这里是将 cache 以 currentNamespace 为Key 放入了一个 HashMap 中。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"二级缓存创建过程三:为每个sql语句绑定 cache"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在生成 Cache 对象后,Mapper 文件会将本 mapper 中所有的语句标签生成一个个 MappedStatement ,在这个过程中,会给每个 statement 绑定上二级缓存,使得他可以直接使用。"}]},{"type":"codeblock","attrs":{"lang":"java"},"content":[{"type":"text","text":"public void parseStatementNode() {\n String id = context.getStringAttribute(\"id\");\n String databaseId = context.getStringAttribute(\"databaseId\");\n\n \/\/ 如果数据库 id 不为空且匹配不上的话,不进行下面的加载工作\n if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {\n return;\n }\n\n String nodeName = context.getNode().getNodeName();\n \/\/ 此处拿的是标签,insert | update | delete | select\n SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));\n \/\/ 是否是 select 语句\n boolean isSelect = sqlCommandType == SqlCommandType.SELECT;\n \/\/ 是否清除缓存\n boolean flushCache = context.getBooleanAttribute(\"flushCache\", !isSelect);\n \/\/ 是否使用二级缓存\n boolean useCache = context.getBooleanAttribute(\"useCache\", isSelect);\n \/\/ 结果是否排序\n boolean resultOrdered = context.getBooleanAttribute(\"resultOrdered\", false);\n \n ······\n \n \/\/ 配置一系列属性,标签上的对应属性可以在这里看到\n StatementType statementType = StatementType.valueOf(context.getStringAttribute(\"statementType\", StatementType.PREPARED.toString()));\n Integer fetchSize = context.getIntAttribute(\"fetchSize\");\n Integer timeout = context.getIntAttribute(\"timeout\");\n String parameterMap = context.getStringAttribute(\"parameterMap\");\n String resultType = context.getStringAttribute(\"resultType\");\n Class resultTypeClass = resolveClass(resultType);\n String resultMap = context.getStringAttribute(\"resultMap\");\n String resultSetType = context.getStringAttribute(\"resultSetType\");\n ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);\n if (resultSetTypeEnum == null) {\n resultSetTypeEnum = configuration.getDefaultResultSetType();\n }\n String keyProperty = context.getStringAttribute(\"keyProperty\");\n String keyColumn = context.getStringAttribute(\"keyColumn\");\n String resultSets = context.getStringAttribute(\"resultSets\");\n\n \/\/ 构建解析完成的 MappedStatement ,也就是将