说明
本文涉及的相关软件版本如下:
- mybatis 3.4.x
- HotSpot JDK1.8
- Windows 11
- IDEA 2022.3
先看一段 mybatis 相关的代码
今天一个朋友丢给我如下一段代码: 然后跟我讲为什么本地是好好的, 发布到线上执行就报错。
- BlogMapper.java
public interface BlogMapper {
List<Blog> select(Integer id, String title);
}
- BlogMapper.xml
<select id="select">
select * from my_blog
<where>
<if test="id!=null">
id = #{id}
</if>
<if test="title!=null">
and title = #{title}
</if>
</where>
</select>
- 报错信息
报错信息也很容易理解: mybatis 动态生成 sql
时,提示参数 id 找不到, 只找到了 [arg1, arg0, param1, param2]
这四个可用的参数名称
Caused by: org.apache.ibatis.binding.BindingException:
Parameter 'id' not found. Available parameters are [arg1, arg0, param1, param2]
mybatis 官方说明
mybatis 官网文档说明如下:
如果你的映射方法接受多个参数,就可以使用这个注解自定义每个参数的名字。否则在默认情况下,除
RowBounds
以外的参数会以 “param” 加参数位置被命名。例如#{param1}
,#{param2}
。如果使用了@Param("person")
,参数就会被命名为#{person}
。
因此我们上面的使用方式明显是不对的, 理论上讲这段程序不管在 线上 还是 本地编辑器 运行, 都是会提示同样的报错的。
mybatis 源码分析
关于 mybatis 是如何把 mapper.java
的 参数名 绑定到 mapper.xml
中 占位符 上的, 可以直接看 ParamNameResolver.java
这个类
源码中重点内容已经 标识 出来, 我们只需要关注 重点1 和 重点2
其实源码注释说得很明白了:
- 如果使用了
@Param("名称")
注解, 就用注解中的名称; - 否则, 就调用
isUseActualParamName()
方法; - 如果还是拿不到, 就由 mybatis 生成。
public static final boolean parameterExists;
static {
boolean available = false;
try {
Resources.classForName("java.lang.reflect.Parameter");
available = true;
} catch (ClassNotFoundException e) {
// ignore
}
parameterExists = available;
}
// java.lang.reflect.Parameter
private String getActualParamName(Method method, int paramIndex) {
if (Jdk.parameterExists) {
return ParamNameUtil.getParamNames(method).get(paramIndex);
}
return null;
}
从上面的源码可以看出, 只要程序能够加载到 java.lang.reflect.Parameter
这个类, 我们就能拿到参数名称。
/**
* Information about method parameters.
*
* A {@code Parameter} provides information about method parameters,
* including its name and modifiers. It also provides an alternate
* means of obtaining attributes for the parameter.
*
* @since 1.8
*/
public final class Parameter implements AnnotatedElement {
}
从这个类的注释可以看出, 这个类是 JDK1.8 才引入的类, 也就是说我们是可以拿到真实参数名称的。 那么为什么还是报错呢?
打断点调试
通过断点调试, 我们可以看到我们拿到的参数名称是 arg0
和 arg1
, 并不是我们期望的 id
和 title
。
这两个名称在我们前面报错信息的可选值范围内,那么 param0
和 param1
是如何生成的呢?
param0
和 param1
是 mybatis 为我们生成的, 用来兜底的, 那么 arg0
和 arg1
是怎么生成的呢 ?
Java 编译
通过《深入理解 Java 虚拟机》 一说, 我们可以知道, 字段名是放在 class 文件, 而 class 文件是在编译期生成的。编译的命令是 javac
那么我们可以尝试查看 javac
命令是否为我们提供了相关参数来帮我们获取参数名称。
我们在命令行工具上执行 javac
命令, 控制台会显示它的所有 可选参数, 其中有一个参数的说明如下:
-parameters 生成元数据以用于方法参数的反射
意思就是 编译程序 时, 如果加上这个参数, 在程序运行过程中,就可以拿到程序中方法中的 参数名称.
那么我们 IDEA 编译如何加上这个参数呢?
除此之外, 我们还可以通过 IDEA 提供的 jclasslib 插件帮我们翻译 class 文件:
从上图可以看出, 当我们加上编译参数时, class 文件中多了一个描述符. 也就是我们方法参数的元数据信息.
找到真凶
原来我朋友之前在使用 IDEA 时, 如果遇到了一些问题(忘记了具体什么问题), 然后稀里糊涂就加了上述配置, 后来也一直没删除,最后今天就碰巧遇上了文中所描述的 “灵异事件”。
根据官方资料再次强化我们的结论
详情请参考 Access to Parameter Names at Runtime
我摘抄了其中一段话:
The proposed approach is to create an optional new JVM attribute in version 52.0 class files to store information about the parameters of a JVM-level method
大致意思是在 JDK8 版本中, 可以 选择性 在 class
文件存储 方法的参数名称.