MyBatis @Param分析
2023-10-30 15:00:56 # Backend # MyBatis

1. 简介

当使用 MyBatis 时,如果接口中抽象方法有多个简单类型参数,MyBatis无法识别抽象方法参数的自定义名称,需要使用 @Param 为每个参数命名,以检查用户登录为例

有注解:

User checkLoginByParam(@Param("name") String username, @Param("pwd") String password);

image-20231026163255722

无注解:

User checkLogin(String username, String password);

image-20231026163359314

2. 使用@Param

测试类代码如下,通过 debug 分析 @Param 在底层如何发挥作用

断点设置为 checkLoginByParam

@Test
public void testCheckLoginByParam() {
SqlSession sqlSession = SqlSessionUtils.getSqlSession();
ParameterMapper mapper = sqlSession.getMapper(ParameterMapper.class);
User user = mapper.checkLoginByParam("hoyt", "907");
System.out.println(user);
}

进入 org.apache.ibatis.binding.MapperProxy.invoke

  • public class MapperProxy<T> implements InvocationHandler, Serializable
  • 从类名可以看出底层使用的是动态代理
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
return Object.class.equals(method.getDeclaringClass()) ? method.invoke(this, args) : this.cachedInvoker(method).invoke(proxy, method, args, this.sqlSession);
} catch (Throwable var5) {
throw ExceptionUtil.unwrapThrowable(var5);
}
}

接下来先调用该类中 cachedInvoker(method) 方法返回 MapperMethodInvoker,再调用其 invoke(proxy, method, args, this.sqlSession)

image-20231026164615912

此处 command 中的 name 即为 MyBatis Mapper 文件中的 命名空间.方法id

execute 方法相对复杂,会对方法的类型分类讨论

public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
Object param;
switch (this.command.getType()) {
case INSERT:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.insert(this.command.getName(), param));
break;
case UPDATE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.update(this.command.getName(), param));
break;
case DELETE:
param = this.method.convertArgsToSqlCommandParam(args);
result = this.rowCountResult(sqlSession.delete(this.command.getName(), param));
break;
case SELECT:
if (this.method.returnsVoid() && this.method.hasResultHandler()) {
this.executeWithResultHandler(sqlSession, args);
result = null;
} else if (this.method.returnsMany()) {
result = this.executeForMany(sqlSession, args);
} else if (this.method.returnsMap()) {
result = this.executeForMap(sqlSession, args);
} else if (this.method.returnsCursor()) {
result = this.executeForCursor(sqlSession, args);
} else {
// 会跳到这里继续执行
param = this.method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(this.command.getName(), param);
if (this.method.returnsOptional() && (result == null || !this.method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + this.command.getName());
}

if (result == null && this.method.getReturnType().isPrimitive() && !this.method.returnsVoid()) {
throw new BindingException("Mapper method '" + this.command.getName() + " attempted to return null from a method with a primitive return type (" + this.method.getReturnType() + ").");
} else {
return result;
}
}

这里也可以看到,传到底层的参数是只有值的,没有保存参数名

image-20231026165252847

执行到 param = this.method.convertArgsToSqlCommandParam(args); 然后该方法继续跳转到 getNamedParams

image-20231026165632710

此处关键点即为 names 成员变量,private final SortedMap<Integer, String> names;,在构造函数中被赋值

public ParamNameResolver(Configuration config, Method method) {
this.useActualParamName = config.isUseActualParamName();
Class<?>[] paramTypes = method.getParameterTypes(); // 获取参数类型
Annotation[][] paramAnnotations = method.getParameterAnnotations(); // 获取所有参数的所有注解, 一维数组是参数,二维是注解
SortedMap<Integer, String> map = new TreeMap();
int paramCount = paramAnnotations.length; // 参数数量

for(int paramIndex = 0; paramIndex < paramCount; ++paramIndex) { // 遍历参数
if (!isSpecialParameter(paramTypes[paramIndex])) {
String name = null;
Annotation[] var9 = paramAnnotations[paramIndex]; // 该参数所有注解
int var10 = var9.length; // 该参数注解个数

for(int var11 = 0; var11 < var10; ++var11) {
Annotation annotation = var9[var11];
if (annotation instanceof Param) {
this.hasParamAnnotation = true;
name = ((Param)annotation).value(); // @Param注解的值,即变量名
break;
}
}

if (name == null) {
if (this.useActualParamName) {
name = this.getActualParamName(method, paramIndex);
}

if (name == null) {
name = String.valueOf(map.size());
}
}

map.put(paramIndex, name); // 将每个参数的下标和名称保存
}
}

this.names = Collections.unmodifiableSortedMap(map);
}

这样就获取到了 @Param 中设置的变量名

image-20231026170320870

回到 getNamedParams 方法

public Object getNamedParams(Object[] args) {
int paramCount = this.names.size();
if (args != null && paramCount != 0) {
if (!this.hasParamAnnotation && paramCount == 1) { // 只有一个变量且没有标注@Param
Object value = args[(Integer)this.names.firstKey()];
return wrapToMapIfCollection(value, this.useActualParamName ? (String)this.names.get(0) : null);
} else { // 标注了@Param或多个变量
Map<String, Object> param = new MapperMethod.ParamMap();
int i = 0;

for(Iterator var5 = this.names.entrySet().iterator(); var5.hasNext(); ++i) {
Map.Entry<Integer, String> entry = (Map.Entry)var5.next();
param.put(entry.getValue(), args[(Integer)entry.getKey()]); // 添加@Param设置的名称
String genericParamName = "param" + (i + 1);
if (!this.names.containsValue(genericParamName)) { // 避免覆盖@Param设置的名称
param.put(genericParamName, args[(Integer)entry.getKey()]); // 添加通用变量名 param1, param2...
}
}

return param;
}
} else {
return null;
}
}

现在的param

image-20231026171340869

3. 不使用@Param

不使用 @Param 只能使用 arg0, arg1… 或者 param1, param2…

param 的情况已经分析过了,现在讨论使用 arg 的情况

<!--User checkLogin(String username, String password);-->
<select id="checkLogin" resultType="User">
select * from t_user where username = #{arg0} and password = #{arg1}
</select>

一样打断点进行分析,大部分和 2 一致,区别在于 ParamNameResolver 的构造函数

if (name == null) {
if (this.useActualParamName) {
name = this.getActualParamName(method, paramIndex); // 进入了这里
}

if (name == null) {
name = String.valueOf(map.size());
}
}

后面就是方法的调用,不详细说明了

image-20231026172836774

image-20231026172855966

image-20231026172913541

image-20231026172936494

image-20231026173139900

image-20231026173338187

最终names为

image-20231026173503741

param为

image-20231026173644373