解析处理parameterMap元素
Tips: 这是一篇正经的blog。
简单了解parameterMap
写这篇博客的时候,我在网上查找了很多资料,发现现在关于parameterMap元素相关的资料越来越少了,一方面是因为parameterMap元素使用的比较少,另一方面也和Mybatis官方已经弃用这个元素有关。
但是这可难不倒我,排除万难,我在ibatis-2项目中的sql-map-2.dtd文件中,找到了关于parameterMap元素的描述:
The parameterMap is responsible for mapping JavaBeans properties to the parameters of a statement. The parameterMap itself only requires a id attribute that is an identifier that statements will use to refer to it. The class attribute is optional but highly recommended. Similar to the parameterClass attribute of a statement, the class attribute allows the framework to validate the incoming parameter as well as optimize the engine for performance. The parameterMap can contain any number of parameter mappings that map directly to the parameters of a statement.
简单理解起来就是:
在
mybatis中,parameterMap元素负责将java对象中的属性定义映射为sql语句的执行参数。
parameterMap元素有一个id属性定义,用于供其他语句引用。和一个建议使用的
class属性:class属性和parameterClass属性类似,可以优化mybatis对parameterMap元素的验证流程和解析操作。一个
parameterMap元素可以配置多条参数映射关系。
Tips:ibatis中的parameterClass在mybatis中名为parameterType。
总结一下:parameterMap元素的作用主要是维护一组java类型和jdbc类型之间的映射关系以及二者之间的转换策略。
应用场景
实际上,parameterMap元素很少被使用,因为他和select|insert|update|delete元素的parameterType属性的作用类似。
而且就连parameterType属性也很少使用,因为mybatis可以通过类型处理器(TypeHandler)推断出具体传入语句的参数类型:
换而言之就是,parameterType属性的值是可以推断出来的,因此parameterType是一个可选属性,不填也没有太大的影响。
为什么我们需要知道用于执行语句的参数类型呢?
这是因为当我们的定义的Mapper方法的入参是一个复杂对象时,我们需要明确的告知mybatis如何从这个复杂对象中获取所需的的属性定义。
最简单的方式就是让mybatis得到复杂对象的类型,再根据对象的属性定义获取实际参数定义。
还有一种方式是声明一个parameterMap元素,然后在parameterMap元素中依次声明每个属性定义。
在将属性转为执行参数的角度上,parameterMap元素和parameterType属性的效果近乎一致,但是除此之外,parameterMap还提供了一些额外的功能。
parameterMap的子元素parameter还有一些额外的属性可以更精确的控制将属性转换为执行参数的过程。
比如:使用指定的类型转换器的typeHandler属性,控制数值精度的scale属性等。
为什么被弃用
那么parameterMap元素为什么会被弃用呢?
这是因为,在mybatis的实现中,parameterMap元素配置的优先级比较低,它只会在一个语句完全没有通过其他途径配置映射的时候才会生效:
// 代码在`MappedStatement`的`getBoundSql`方法中
if (parameterMappings == null || parameterMappings.isEmpty()) {
boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
}
假设我们使用了行内参数映射配置了一条属性转为执行参数的规则,那么整个parameterMap都不会生效:
<parameterMap id="account" type="org.apache.learning.parameter_map.Account">
<parameter property="id" javaType="int"/>
<parameter property="name" javaType="string" jdbcType="VARCHAR"/>
<parameter property="balance" javaType="double" jdbcType="DOUBLE"/>
</parameterMap>
<insert id="insert" parameterMap="account">
insert into accounts (id, name, balance)
values (#{id}, #{name}, #{balance})
</insert>
注意看,上面这种写法,名为account的parameterMap配置不会生效,如果想要是parameterMap配置生效,我们需要使用下面这种写法:
<parameterMap id="account" type="org.apache.learning.parameter_map.Account">
<parameter property="id" javaType="int"/>
<parameter property="name" javaType="string" jdbcType="VARCHAR"/>
<parameter property="balance" javaType="double" jdbcType="DOUBLE"/>
</parameterMap>
<insert id="insert" parameterMap="account">
insert into accounts (id, name, balance)
values (?, ?, ?)
</insert>
以?取代行内参数映射配置,这种写法带来了什么问题呢?
观看上面的比对效果图,我们会发现使用行内参数映射比parameterMap配置看起来更直观,阅读性更强,这里只是三个参数,所以是三个?,如果我们有100个参数呢?
而且,这里的sql并不复杂,如果我们定义更复杂的sql,sql中需要对同一属性进行多次引用,那么构建parameterMap配置将会是一件费力不讨好的事情:
SELECT *
FROM ORDER_DETAILS od
WHERE
(od.status =#{status} AND od.type="类型1")
OR
(od.status =#{status} AND od.type="类型2")
所以虽然行内参数映射牺牲了一定的复用性,但是会让代码更直观,更简洁。
而且,如果我们的实现需要代码复用,同样可以选择使用sql元素将行内参数映射包装成sqlFragment来进行复用。
sql元素的解析很快就会讲到。
因此使用行内参数映射来取代parameterMap元素声明是一个更好的方案,所以parameterMap元素就被弃用了。
探究parameterMap元素
parameterMap的DTD元素定义
parameterMap在Mybatis中的DTD定义如下:
<!ELEMENT parameterMap (parameter+)?>
<!ATTLIST parameterMap
id CDATA #REQUIRED
type CDATA #REQUIRED
>
parameterMap有两个必填的属性,其中id属性用于定义parameterMap在当前mapper文件中的唯一标志,type则表示parameterMap的具体类型。
在parameterMap下可以出现一个或多个parameter子元素,parameter子元素的DTD定义如下:
<!ELEMENT parameter EMPTY>
<!ATTLIST parameter
property CDATA #REQUIRED
javaType CDATA #IMPLIED
jdbcType CDATA #IMPLIED
mode (IN | OUT | INOUT) #IMPLIED
resultMap CDATA #IMPLIED
scale CDATA #IMPLIED
typeHandler CDATA #IMPLIED
>
parameter元素有七个属性,除了property属性是必填的以外,其他参数均是非必填的:
property参数用于指定参数的名称,为必填项。javaType用来指定参数的java类型。jdbcType用来指定参数的JDBC类型。mode用来配置参数的类型,其中IN表示入参,OUT表示出参,INOUT表示即为出参也为入参。resultMap参数,当mode参数为OUT/INOUT,且jdbcType为CURSOR的时候,需要指定一个resultMap来映射参数结果集。scale参数用来指定参数小数保留位数,比较有趣的是,虽然DTD中定义的是scale,但是Mybatis实际解析的却是numericScale.typeHandler用来指定处理参数在java类型和jdbc类型之间互相转换的转换器类型。
每一个parameter元素,都控制着对应的java属性值如何转换为sql参数。
解析的整体流程
关于parameterMap元素的解析过程也相对比较简单,这是解析处理的大致流程:
@startuml
(*) --> "获取mapper中定义的所有parameterMap元素"
if "是否有未处理的parameterMap元素" as parameterMapIf then
partition 解析parameterMap元素 #DeepSkyBlue{
--> [有] 依次加载parameterMap元素的id和type属性
--> 获取当前parameterMap元素的所有parameter子节点
if "是否有未处理的parameter子元素" as parameterIf then
partition 解析parameter子元素 #Azure{
--> [有] "依次加载parameter元素的\nproperty,javaType,\njdbcType,resultMap,\nmode,typeHandler,numericScale属性"
--> "创建ParameterMapping对象,并保存起来"
--> parameterIf
}
else
--> [无] "根据id和type属性以及\n解析出的ParameterMapping对象集合,\n创建ParameterMap对象"
--> 将ParameterMap对象注册到参数映射对象注册表中
--> parameterMapIf
}
-->(*)
endif
else
-->[无](*)
endif
header parameterMap元素的解析过程
@enduml
然后,我们来详细的一步一步的看解析过程:
加载所有parameterMap元素
parameterMap元素的解析入口在XMLMapperBuilder的configurationElement()方法中:
// 解析并注册parameterMap元素
parameterMapElement(context.evalNodes("/mapper/parameterMap"));
在获取需要解析的parameterMap元素时,使用的XNode#evalNodes(String expression)方法和之前在获取元素时使用的XNode#evalNode(String expression)稍微有些不一样,该方法返回的是一组XNode对象。
因此调用context.evalNodes("/mapper/parameterMap")方法将会获取到当前mapper中的所有parameterMap元素定义。
单个parameterMap元素的解析工作主要分为三步:
- 获取当前
parameterMap元素的唯一标志声明——解析id属性; - 获取当前
parameterMap元素对应的java对象的类型——解析type属性; - 将
parameter子元素解析成对应的ParameterMapping对象,并存入parameterMappings集合中;
完成上面三步之后,将获得的数据作为参数传递给MapperBuilderAssistant的addParameterMap()方法即可完成parameterMap元素的处理工作。
/**
* 解析parameterMap节点集合
*
* @param list parameterMap节点集合
*/
private void parameterMapElement(List<XNode> list) {
// 遍历处理每一个parameterMap节点
for (XNode parameterMapNode : list) {
// 获取parameterMap的唯一标志
String id = parameterMapNode.getStringAttribute("id");
// 获取parameterMap的类型的名称
String type = parameterMapNode.getStringAttribute("type");
// 解析出parameterMap的类型
Class<?> parameterClass = resolveClass(type);
// 获取所有parameter子节点
List<XNode> parameterNodes = parameterMapNode.evalNodes("parameter");
List<ParameterMapping> parameterMappings = new ArrayList<>();
// ...
// 省略解析处理`parameter`元素定义的代码块
// ...
// 注册参数映射表
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
}
}
id属性和type类型的解析都是比较常见的取值操作,相对来说,parameter子元素的解析工作看起来就复杂多了。
解析parameter子元素
parameter子元素对应着ParameterMapping对象,每一个负责参数映射的parameter子元素都会被创建为一个ParameterMapping实例。
ParameterMapping中定义了10个属性,除了和parameter子元素一一对应的七个属性外,还有三个额外的属性:
其中expression属性没有任何实际意义,在设计之初,这个属性的应该是想允许用户提供一个表达式,动态的从mybatis运行环境中加载数据,获得更高的自由度的同时,增强ParameterMapping的能力。
比如,可以利用expression属性增强property属性,实现在运行期间根据入参对象的不同,读取不同属性的能力:
我们可以简单构造一个实现方案,创建一个Expressions上下文环境,用来解析我们的表达式,在这个上下文环境内,我们提供了两个属性_parameter和_this。
其中_parameter表示运行期间mybatis获得的用于执行Sql的方法入参对象,_this属性指向ParameterMapping对象本身。
public class Express {
@Test
@SneakyThrows
public void ognlExpress() {
ParameterMapping mapping = new ParameterMapping.Builder(Mockito.spy(Configuration.class), "id", Object.class)
.javaType(String.class)
.jdbcType(JdbcType.VARCHAR)
.build();
// 构建运行环境上下文
Map<Object, Object> values = new HashMap<>();
values.put("_this", mapping);
values.put("_parameter", new Object());
// 解析表达式
OgnlCache.getValue("_this.property=(_parameter instanceof org.apache.learning.Express)?\"id\":\"newId\"", values);
// 成功取值
Assertions.assertEquals("newId", mapping.getProperty());
}
}
上面只是一个简单的示例,如果真的有需要,可以考虑扩展mybatis的方法来实现类似的功能。
除了尚未使用的expression属性之外,jdbcTypeName属性用来指定存储过程中的出参类型名称:
CallableStatementHandler的registerOutputParameters()方法:
if (parameterMapping.getJdbcTypeName() == null) {
// 配置出参
cs.registerOutParameter(i + 1, parameterMapping.getJdbcType().TYPE_CODE);
} else {
// 配置出参
cs.registerOutParameter(i + 1, parameterMapping.getJdbcType().TYPE_CODE, parameterMapping.getJdbcTypeName());
}
configuration属性作为Mybatis全局配置对象的引用,我们已经很熟悉了。
基本属性的解析
回到正题,解析parameter子元素,首先通过常规的取值方式,获取parameter元素的属性定义:
// 参数名称
String property = parameterNode.getStringAttribute("property");
// 参数Java类型
String javaType = parameterNode.getStringAttribute("javaType");
// 参数jdbc类型
String jdbcType = parameterNode.getStringAttribute("jdbcType");
// 返回类型
String resultMap = parameterNode.getStringAttribute("resultMap");
// 该参数主要用于存储过程,分别:IN 表示入参,OUT表示出参,INOUT表示出入参
String mode = parameterNode.getStringAttribute("mode");
// 指定类型处理器
String typeHandler = parameterNode.getStringAttribute("typeHandler");
// 确定小数点后保留的位数
Integer numericScale = parameterNode.getIntAttribute("numericScale");
比较有意思的是,在parameter元素的dtd声明中,用于控制数值精度的是scale属性,但是在解析时读取的却是numericScale属性:
这就意味着在parameter元素中配置的scale属性不会生效。
这是一个小bug还是一个小彩蛋呢?
构建ParameterMapping对象
在获取到所有的参数之后,Mybatis将生成ParameterMapping实例的具体操作交给了映射器构建助手MapperBuilderAssistant的buildParameterMapping()方法来完成:
// 参数名称
String property = parameterNode.getStringAttribute("property");
// 参数Java类型
String javaType = parameterNode.getStringAttribute("javaType");
// 参数jdbc类型
String jdbcType = parameterNode.getStringAttribute("jdbcType");
// 返回类型
String resultMap = parameterNode.getStringAttribute("resultMap");
// 该参数主要用于存储过程,分别:IN 表示入参,OUT表示出参,INOUT表示出入参
String mode = parameterNode.getStringAttribute("mode");
// 指定类型处理器
String typeHandler = parameterNode.getStringAttribute("typeHandler");
// 确定小数点后保留的位数
Integer numericScale = parameterNode.getIntAttribute("numericScale");
// 解析参数类型
ParameterMode modeEnum = resolveParameterMode(mode);
// 解析java类型
Class<?> javaTypeClass = resolveClass(javaType);
// 解析jdbc类型
JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType);
// 解析出类型处理器
Class<? extends TypeHandler<?>> typeHandlerClass = resolveClass(typeHandler);
ParameterMapping parameterMapping = builderAssistant.buildParameterMapping(parameterClass, property, javaTypeClass, jdbcTypeEnum, resultMap, modeEnum, typeHandlerClass, numericScale);
// 添加到已解析出的参数映射表
parameterMappings.add(parameterMapping);
在buildParameterMapping()中,Mybatis调用了MapperBuilderAssistant的applyCurrentNamespace(String base, boolean isReference)方法来完成对ParameterMapping引用的resultMap的唯一标志的处理,主要是将可能是局部引用的resultMap属性值转换为全局唯一的ID属性引用。
/**
* 将指定的标志和当前的命名空间合并
*
* @param base 指定的标志
* @param isReference 是否允许引用其他命名空间
* @return 合并后的标志
*/
public String applyCurrentNamespace(String base, boolean isReference) {
if (base == null) {
return null;
}
if (isReference) {
// 在允许引用其他命名空间的元素的场景下,如果包含了`.`,则表示已经引用了其他命名空间.
if (base.contains(".")) {
return base;
}
} else {
// 在不允许引用其他命名空间的元素的场景下,如果是以当前mapper元素的命名空间+. 开头,则表示使用的是包含了当前
// 命名空间标志的名称.
if (base.startsWith(currentNamespace + ".")) {
return base;
}
// 如果不是使用的当前命名空间,同时名称里面还包含了.,那么这是一个不合格的命名方式。
if (base.contains(".")) {
throw new BuilderException("Dots are not allowed in element names, please remove it from " + base);
}
}
// 合并当前mapper的命名空间和当前元素的唯一标志生成一个全局唯一的新标志
return currentNamespace + "." + base;
}
获取参数的java类型
之后调用resolveParameterJavaType方法解析出参数对应的java类型:
// 解析参数的java类型
private Class<?> resolveParameterJavaType(Class<?> resultType, String property, Class<?> javaType, JdbcType jdbcType) {
if (javaType == null) {
if (JdbcType.CURSOR.equals(jdbcType)) {
// 游标对应ResultSet
javaType = java.sql.ResultSet.class;
} else if (Map.class.isAssignableFrom(resultType)) {
// Map对应Object
javaType = Object.class;
} else {
// 通过反射获取java类型
MetaClass metaResultType = MetaClass.forClass(resultType, configuration.getReflectorFactory());
javaType = metaResultType.getGetterType(property);
}
}
if (javaType == null) {
// 默认为Object
javaType = Object.class;
}
return javaType;
}
解析java类型的逻辑是:
如果用户指定了java类型,那就使用用户指定的java类型,如果用户没有指定,则按照当前已有数据推导出java类型。
推导的依据是:
-
如果用户指定了
jdbc类型且类型为JdbcType.CURSOR,那就意味着当前java类型是ResultSet。 -
如果当前
jdbc未指定或者不是JdbcType.CURSOR,那就根据声明的parameterMap的type属性值进行推导。
有了一个属性的名称以及声明该属性的类型定义,通过反射,可以很轻易的得到该属性的类型,这很好理解。
那么,为什么在jdbc类型是JdbcType.CURSOR时,java类型默认是ResultSet呢?
这是因为在jdbc规范中要求ResultSet提供一个可保持的游标,ResultSet可以通过操作这个游标来一步一步的处理行。
可以参考Scrollable_cursors中关于数据库游标的定义。
因此,当jdbc类型是JdbcType.CURSOR时,很自然的我们就会想到,我们可以将其转换为ResultSet来进行数据的操作。
获取类型处理器
在获取java类型之后,resolveTypeHandler()方法负责根据用户指定的typeHandlerType来获取类型转换器:
/**
* 解析出指定的类型处理器
*
* @param javaType java类型
* @param typeHandlerType 类型处理器的类型
* @return 类型处理器实例
*/
protected TypeHandler<?> resolveTypeHandler(Class<?> javaType, Class<? extends TypeHandler<?>> typeHandlerType) {
if (typeHandlerType == null) {
return null;
}
// javaType ignored for injected handlers see issue #746 for full detail
TypeHandler<?> handler = typeHandlerRegistry.getMappingTypeHandler(typeHandlerType);
if (handler == null) {
// 创建一个新的类型处理器
// not in registry, create a new one
handler = typeHandlerRegistry.getInstance(javaType, typeHandlerType);
}
return handler;
}
负责存储和获取类型转换器的TypeHandlerRegistry对象,我们在前面已经做了非常深入的了解,因此,这里只大概看一下加载逻辑,更具体的加载细节,可以回头看解析TypeHandlerRegistry对象相关的文章。
参数齐全,构建ParameterMapping对象
ok,通过上面的一些列操作,我们成功的获取了构建一个ParameterMapping对象需要的所有参数,这时候,我们就可以借助于ParameterMapping的建造器对象ParameterMapping.Builder来完成一个ParameterMapping实例的创建工作了。
return new ParameterMapping.Builder(
configuration/*Mybatis配置*/
, property/*参数名称*/
, javaTypeClass /*java类型*/
)
.jdbcType(jdbcType)/*参数的jdbc类型*/
.resultMapId(resultMap)/*引用的resultMap的全局唯一标志*/
.mode(parameterMode)/*参数类型*/
.numericScale(numericScale)/*小数保留位数*/
.typeHandler(typeHandlerInstance)/*类型转换处理器*/
.build();
在
ParameterMapping.Builder的构造方法中,为参数类型mode提供了默认值——ParameterMode.IN:
public Builder(Configuration configuration, String property, Class<?> javaType) {
parameterMapping.configuration = configuration;
parameterMapping.property = property;
parameterMapping.javaType = javaType;
// 默认类型为入参
parameterMapping.mode = ParameterMode.IN;
}
剩余的方法除了build()方法之外,皆是简单的赋值操作,只有build()方法进行了一些简单的逻辑处理:
public ParameterMapping build() {
// 解析出来类型转换器
resolveTypeHandler();
// 验证
validate();
return parameterMapping;
}
resolveTypeHandler()方法负责在当前ParameterMapping对象未被指定TypeHandler实例时,根据javaType属性解析出有效可用的TypeHandler实例:
/**
* 解析出类型转换器
*/
private void resolveTypeHandler() {
if (parameterMapping.typeHandler == null && parameterMapping.javaType != null) {
// 通过java类型获取类型转换器
// 获取Mybatis配置
Configuration configuration = parameterMapping.configuration;
// 获取类型转换器注册表
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
// 通过类型转换器注册表获取类型转换器
parameterMapping.typeHandler = typeHandlerRegistry.getTypeHandler(parameterMapping.javaType, parameterMapping.jdbcType);
}
}
validate()方法则负责进行必要的属性验证:
/**
* 验证当前ResultMapping
*/
private void validate() {
// 验证
if (ResultSet.class.equals(parameterMapping.javaType)) {
if (parameterMapping.resultMapId == null) {
// 是否有必须的resultMapId
throw new IllegalStateException("Missing resultmap in property '"
+ parameterMapping.property + "'. "
+ "Parameters of type java.sql.ResultSet require a resultmap.");
}
} else {
if (parameterMapping.typeHandler == null) {
// 是否有对应的类型转换器
throw new IllegalStateException("Type handler was null on parameter mapping for property '"
+ parameterMapping.property + "'. It was either not specified and/or could not be found for the javaType ("
+ parameterMapping.javaType.getName() + ") : jdbcType (" + parameterMapping.jdbcType + ") combination.");
}
}
}
- 如果当前
ParameterMapping对象javaType属性是ResultSet,那么就必须提供resultMapId属性,resultMapId属性用于获取负责描述查询结果的ResultMap对象。 - 如果当前
ParameterMapping对象javaType属性不是ResultSet,那么就必须能够获取有效的typeHandler来供mybatis完成类型转换的工作。
到此整个ParameterMapping对象的解析操作就已经完成了,我们继续回到构建ParameterMap对象的过程中。
构建ParameterMap对象
ParameterMap对象的定义比较简单,他只有三个属性:
/**
* 唯一标志
*/
private String id;
/**
* 入参对象类型
*/
private Class<?> type;
/**
* 所有参数的具体描述对象集合
*/
private List<ParameterMapping> parameterMappings;
在前面的解析过程中,我们已经得到了创建一个ParameterMap对象所需的所有参数,接下来要做的就是借助于MapperBuilderAssistant的addParameterMap()方法将这些参数转换为ParameterMap对象:
builderAssistant.addParameterMap(id, parameterClass, parameterMappings);
public ParameterMap addParameterMap(String id, Class<?> parameterClass, List<ParameterMapping> parameterMappings) {
// 拼接命名空间和唯一ID
id = applyCurrentNamespace(id, false);
// 构建一个参数映射表
ParameterMap parameterMap = new ParameterMap.Builder(configuration, id, parameterClass, parameterMappings).build();
// 添加参数映射表
configuration.addParameterMap(parameterMap);
return parameterMap;
}
在addParameterMap方法中,Mybatis首先会调用applyCurrentNamespace方法来将当前的ParameterMap的ID转换为全局唯一的标志。
之后借助于ParameterMap的建造器ParameterMap.Builder对象来完成一个ParameterMap对象的创建工作。
ParameterMap的构建方法相对比较简单,除了简单赋值操作之外,他只是简单的移除了传入的parameterMappings集合的可变性。
public static class Builder {
private ParameterMap parameterMap = new ParameterMap();
public Builder(Configuration configuration, String id, Class<?> type, List<ParameterMapping> parameterMappings) {
parameterMap.id = id;
parameterMap.type = type;
parameterMap.parameterMappings = parameterMappings;
}
public Class<?> type() {
return parameterMap.type;
}
public ParameterMap build() {
//lock down collections
parameterMap.parameterMappings = Collections.unmodifiableList(parameterMap.parameterMappings);
return parameterMap;
}
}
到这里,我们也完成了一个ParameterMap对象的创建工作。
注册ParameterMap对象
最后,我们只需要调用Configuration对象的的addParameterMap()方法将ParameterMap对象注册到Configuration对象维护的参数映射器注册表parameterMaps集合中即可。
/**
* 参数映射表
*/
protected final Map<String, ParameterMap> parameterMaps = new StrictMap<>("Parameter Maps collection");
public void addParameterMap(ParameterMap pm) {
parameterMaps.put(pm.getId(), pm);
}
写在最后
parameterMap元素的解析工作,写的比较细,忙里偷闲的断断续续写了四天才完成。
用了这么长时间,最主要的原因还是parameterMap元素已被弃用,本身用的比较少,相对比较陌生,之前看源码的时候,关于parameterMap元素基本也是粗略的看一遍,没有过多的深入研究,另一方面是相关的资料也比较难找。
不管怎样,parameterMap元素的解析工作是梳理完了,给自己点个赞。