看名字就知道,ParamNameResolver是MyBatis用来解析方法参数的。我们调用Mapper接口的方法进行传参,经过ParamNameResolver解析后,xml文件里的SQL语句就可以使用这些参数了,就可以完成动态SQL和占位符的参数填充。
MyBatis是如何解析参数的?如何在xml里更好的使用这些参数呢?今天我们从源码的角度来分析下。
1. 源码分析
ParamNameResolver类结构很简单,没有继承和实现,源码很好看懂。它的职责也非常单一:解析方法形参存放到SortedMap、根据实参完成参数名和参数值的映射得到ParamMap。
1.1 属性
public class ParamNameResolver {
// 自动生成的参数名前缀
public static final String GENERIC_NAME_PREFIX = "param";
// 是否使用实际的参数名(反射获取)
private final boolean useActualParamName;
// 参数下标-对应的参数名 映射关系
private final SortedMap<Integer, String> names;
// 是否存在@Param注解
private boolean hasParamAnnotation;
}
如果参数没有加@Param注解,MyBatis会使用反射去获取参数名。但是,当源码被编译成字节码文件后,参数名称对于程序的执行实际上是没有任何意义的,因此编译时默认会去掉参数名称,反射获取到的参数名是arg0,arg1这种无意义的名称。那如何才能通过反射获取到参数名呢?
只有当JDK版本为8及以上,且编译时加上了**-parameters**参数,才能通过反射获取到真实的参数名。
正是因为开发者可能漏加了注解,或者反射获取不到参数名称,这两者充满了不确定性,因此MyBatis提供了一种确定性的解决方案,那就是为参数自动生成名称,常量GENERIC_NAME_PREFIX就是自动生成的名称前缀,生成规则为:【param+序号】,因此你也完全可以在xml中通过【param+序号】来使用参数。
useActualParamName代表是否使用实际的参数名称,也就是反射获取的参数名称。上面已经说过了,要想反射获取到真实的参数名门槛是比较高的,得到的很可能是无意义的参数名称。该值默认为true,当配置为false时,MyBatis则自动使用参数所在下标作为参数名,此时你可以在xml中通过#{0}来使用第一个参数。
names是一个有序Map容器,它存储的是方法形参中,参数下标对应的参数名称。
hasParamAnnotation记录的是方法形参中,是否存在@Param注解。
1.2 构造函数
属性看完,我们接下来看看构造函数。
/**
* 1.优先获取@Param注解的值
* 2.通过反射获取参数名
* 3.使用参数下标
*/
public ParamNameResolver(Configuration config, Method method) {
// 从配置对象中获取:是否使用实际参数名称 <setting name="useActualParamName" value="true" />
this.useActualParamName = config.isUseActualParamName();
// 反射获取方法参数类型
final Class<?>[] paramTypes = method.getParameterTypes();
// 反射获取参数注解
final Annotation[][] paramAnnotations = method.getParameterAnnotations();
// 存放 参数下标对应参数名称的Map容器
final SortedMap<Integer, String> map = new TreeMap<>();
int paramCount = paramAnnotations.length;
// get names from @Param annotations
for (int paramIndex = 0; paramIndex < paramCount; paramIndex++) {
if (isSpecialParameter(paramTypes[paramIndex])) {
/**
* 跳过RowBounds和ResultHandler参数,这两个参数不做解析。
* RowBounds:处理分页
* ResultHandler:处理结果
*/
continue;
}
String name = null;
for (Annotation annotation : paramAnnotations[paramIndex]) {
if (annotation instanceof Param) {
// 如果加了@Param注解,则取注解的值
hasParamAnnotation = true;
name = ((Param) annotation).value();
break;
}
}
// 没有加@Param注解
if (name == null) {
// @Param was not specified
if (useActualParamName) {
// 如果使用实际的参数名,则通过反射获取参数名。
// JDK8编译类加–parameters参数可以保留参数名称,否则得到的是arg0,arg1这种无意义的参数名
name = getActualParamName(method, paramIndex);
}
if (name == null) {
// 如果名称还是为空,则可以使用下标来获取参数:#{param1},#{param2}...
name = String.valueOf(map.size());
}
}
map.put(paramIndex, name);
}
// 使其不可变
names = Collections.unmodifiableSortedMap(map);
}
构造函数主要做了这么几件事:
- 反射获取参数类型。
- 如果参数类型为RowBounds或ResultHandler则跳过解析。
- 反射获取方法形参注解。
- 如果加了@Param注解,则参数名直接使用注解值。
- 如果没有注解,则判断是否需要反射获取参数名。
- 否则使用参数下标作为参数名。
- 映射参数下标和参数名称的关系,构建Map容器。
整个流程还是很清晰易懂的,这里举几个示例。
void select(String name, int age);
解析的names结果为:
-- 编译时不加-parameters参数
-- 使用实际参数名
{
0:"arg0",
1:"arg1"
}
-- 不使用实际参数名
{
0:"0",
1:"1"
}
-- 编译时加了-parameters参数,且使用实际参数名
{
0:"name",
1:"age"
}
加了注解的情况:
void select(@Param("name") String name,@Param("age") int age);
解析的names结果为:
{
0:"name",
1:"age"
}
参数中有RowBounds或ResultHandler的情况:
void select(@Param("name") String name,RowBounds rowBounds,ResultHandler resultHandler,@Param("age") int age);
解析的names结果为:
-- 跳过RowBounds和ResultHandler
{
0:"name",
3:"age"
}
1.3 getNamedParams()
构造函数解析完names,就知道方法参数下标对应的参数名称了,接下来就可以调用getNamedParams()方法根据实参来解析参数名和参数值的映射ParamMap了。
ParamMap继承自HashMap,只不过它将Key的泛型限制为String了。这也很好理解,参数名称肯定是字符串嘛,而参数值就不确定了,使用Object即可。
下面是源码:
/**
* 获取参数名对应的参数值
* 一般是Map结构,当参数只有1个时,直接返回,xml中写任意值都可以匹配到
* @param args
* @return
*/
public Object getNamedParams(Object[] args) {
// 参数数量
final int paramCount = names.size();
if (args == null || paramCount == 0) {
// 无参情况
return null;
} else if (!hasParamAnnotation && paramCount == 1) {
// 没有@Param注解,且参数只有一个的情况
Object value = args[names.firstKey()];
// 如果参数数集合类型,参数名会封装成:collection/list/array
return wrapToMapIfCollection(value, useActualParamName ? names.get(0) : null);
} else {
final Map<String, Object> param = new ParamMap<>();
int i = 0;
for (Map.Entry<Integer, String> entry : names.entrySet()) {
// 参数名 对应 实参值
param.put(entry.getValue(), args[entry.getKey()]);
// add generic param names (param1, param2, ...)
// 额外再自动生成一个参数名映射:param1,param2...
final String genericParamName = GENERIC_NAME_PREFIX + (i + 1);
// ensure not to overwrite parameter named with @Param
if (!names.containsValue(genericParamName)) {
param.put(genericParamName, args[entry.getKey()]);
}
i++;
}
return param;
}
}
该方法会得到一个ParamMap,里面存放着参数名和参数值的映射关系,调用该方法需要传递方法实参数组。
它主要做了这么几件事:
- 如果参数为空,直接返回null。
- 没有@Param注解,且参数只有一个。
- 是否是集合类型(包含数组)?
- 是,则参数名自动设置为collection/list/array。
- 不是,则直接返回参数值。
- 创建ParamMap,将参数名和对应的参数值添加到容器中。
- 额外再生成以【param+序号】为参数名的映射条目。
- 返回Map容器。
对于参数只有一个的情况,且没有@Param注解,并且参数类型为Collection或List或数组的,MyBatis会自动以collection/list/array作为参数名来生成映射关系,你可以在xml中直接使用,如#{list}。源码如下:
/*
如果是集合,可以通过collection访问
如果是List,则可以通过list访问
如果是数组,则可以通过array访问
否则,直接直接返回
*/
public static Object wrapToMapIfCollection(Object object, String actualParamName) {
if (object instanceof Collection) {
ParamMap<Object> map = new ParamMap<>();
map.put("collection", object);
if (object instanceof List) {
map.put("list", object);
}
Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
return map;
} else if (object != null && object.getClass().isArray()) {
ParamMap<Object> map = new ParamMap<>();
map.put("array", object);
Optional.ofNullable(actualParamName).ifPresent(name -> map.put(name, object));
return map;
}
return object;
}
2. 核心点
2.1 如何反射获取参数名?
默认获取到的是arg0,arg1这样的无意义名称,只有JDK版本为8及以上,且编译时加了-parameters参数才能反射获取到。
2.2 SpringMVC可以根据参数名绑定参数,MyBatis为何不行?
SpringMVC不是JDK8版本,没有加-parameters参数,没有加注解,依然可以根据参数名来动态绑定参数,为什么MyBatis就不行?必须加@Param注解?
那是因为Spring的Maven项目在编译打包时自动加了-g参数,这个参数就是告诉编译器,我们需要调试类的信息,这时编译器在编译时,就会保留局部变量表的信息,参数也是局部变量表的一部分。此时Spring再通过ASM去解析字节码文件通过局部变量表变向的获取参数名称,再根据参数名做参数的动态绑定。
总结就是SpringMVC能获取到参数名是因为编译是加了-g参数,编译时保留了【局部变量表】的信息,通过局部变量表变向获取参数名。而MyBatis的Mapper是接口,没有方法体,也就不存在局部变量表了,因此ASM也无能为例了。
3. 总结
本篇文章分析了ParamNameResolver源码,介绍了MyBatis解析参数的内幕。它先是在构造函数中对方法的形参做解析,得到参数下标对应的参数名称,再根据方法实参去做映射,最终得到参数名和参数值的映射关系ParamMap。
关于获取参数名称,还做了额外的扩展,以及和SpringMVC的对比,SpringMVC不用加注解也能获取到参数名称就是因为Controller的方法有方法体,而Mapper是接口,方法不存在方法体,没有局部变量表,也就无法获取。
除了解析能获取到的参数名,MyBatis还会额外生成一条以【param+序号】为参数名的映射关系,即使你漏加了注解,反射获取不到参数名称,也至少保证了一种方法可以正确的使用参数。