本地运行好好的 Java 程序, 一发布到线上就报错的灵异事件终于让我碰到了

785 阅读4分钟

说明

本文涉及的相关软件版本如下:

  • mybatis 3.4.x
  • HotSpot JDK1.8
  • Windows 11
  • IDEA 2022.3

一、关于 mapper 的定义和使用

今天一个朋友丢给我如下一段代码: 然后跟我讲为什么本地是好好的, 发布到线上执行就报错。

  • 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

<img src="C:\Users\iceberg\AppData\Roaming\Typora\typora-user-images\image-20221215225532268.png" alt="image-20221215225532268" style="zoom:50%;" />

其实源码注释说得很明白了:

  • 如果使用了 @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 才引入的类, 也就是说我们是可以拿到真实参数名称的。 那么为什么还是报错呢?

四、断点调试 mybatis 源码

通过断点调试, 我们可以看到我们拿到的参数名称是 arg0arg1, 并不是我们期望的 idtitle

<img src="C:\Users\iceberg\AppData\Roaming\Typora\typora-user-images\image-20221215232451994.png" alt="image-20221215232451994" style="zoom:50%;" />

这两个名称在我们前面报错信息的可选值范围内,那么 param0param1 是如何生成的呢? 在这里插入图片描述

param0param1mybatis 为我们生成的, 用来兜底的, 那么 arg0arg1 是怎么生成的呢 ?

五、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 文件存储 方法的参数名称.