scaventz

ByteBuffer 相关的兼容性问题

JDK 是向下兼容的,这意味着:

  1. 为旧版本JDK编写的Java代码,可以在新发布的JDK上进行编译
  2. 使用旧版本JDK编译生成的class文件,可以在新发布的JRE上运行

但是如果有这样一个项目,它既可以用JDK8编译运行,也可以用JDK9编译运行,当我用Maven打包时,将Maven的运行环境设置为JDK9,将目标字节码版本设置为JDK8(从class文件的bytecode version看是52.0)。那这个包能在 JRE8 下运行吗。

答案是:不一定。

这个问题是我在研究 lwjgl/debug 项目时发现的,该项目是一个 javaagent 项目,至少需要支持JDK8,但由于诸多原因,项目本身使用JDK13来进行开发。该项目默认使用字节码级别为 JDK8 来进行编译。但最终在JDK8上运行时报出

Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.rewind()Ljava/nio/ByteBuffer;

复现该问题

可以使用一个简单的demo来复现该问题,源代码很简单

import java.nio.ByteBuffer;

public class Problem {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocateDirect(14);
        buffer.rewind();
    }
}

将开发环境设置为 JDK 9

image-20201117163100992

这里选择 8 和 9 都可以,不影响

image-20201117163224152

字节码版本选择 1.8

image-20201117163343927

确保打包环境使用JDK9

D:\Github\common-learn\package-problem>java -version
openjdk version "9.0.4"
OpenJDK Runtime Environment (build 9.0.4+11)
OpenJDK 64-Bit Server VM (build 9.0.4+11, mixed mode)

确保打包目标平台为 JDK8

<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>

然后进行编译

D:\Github\common-learn\package-problem>mvn clean package
[INFO] Scanning for projects...
[INFO]
[INFO] --------------------< org.example:package-problem >---------------------
[INFO] Building package-problem 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ package-problem ---
[INFO] Deleting D:\Github\common-learn\package-problem\target
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ package-problem ---
[INFO] Using \'UTF-8\' encoding to copy filtered resources.
[INFO] Copying 0 resource
[INFO]
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ package-problem ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to D:\Github\common-learn\package-problem\target\classes
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ package-problem ---
[INFO] Using \'UTF-8\' encoding to copy filtered resources.
[INFO] skip non existing resourceDirectory D:\Github\common-learn\package-problem\src\test\resources
[INFO]
[INFO] --- maven-compiler-plugin:3.1:testCompile (default-testCompile) @ package-problem ---
[INFO] Nothing to compile - all classes are up to date
 ]
[INFO] --- maven-surefire-plugin:2.12.4:test (default-test) @ package-problem ---
[INFO] No tests to run.
[INFO]
[INFO] --- maven-jar-plugin:2.4:jar (default-jar) @ package-problem ---
[INFO] Building jar: D:\Github\common-learn\package-problem\target\package-problem-1.0-SNAPSHOT.jar
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  1.161 s
[INFO] Finished at: 2020-11-17T16:38:48+08:00
[INFO] ------------------------------------------------------------------------

编译好的 Problem.class 文件从打好的jar包中取出来,然后在 JDK8 环境下运行:

D:\Github\common-learn\package-problem\target>java -version
openjdk version "1.8.0_275"
OpenJDK Runtime Environment (AdoptOpenJDK)(build 1.8.0_275-b01)
OpenJDK 64-Bit Server VM (AdoptOpenJDK)(build 25.275-b01, mixed mode)

D:\Github\common-learn\package-problem\target>java Problem
Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.rewind()Ljava/nio/ByteBuffer;
        at Problem.main(Problem.java:6)

原因

rewind 这个方法,在 JDK8 中,只存在于 Buffer中,而在JDK9中,ByteBuffer 也 override 了这个方法。所以初次遇到这个问题,大概就会往这个方向猜,但是又会很困惑,即使JDK8里的 ByteBuffer 里没有 rewind 方法,可是它的父类里有啊,怎么可能 NoSuchMethod呢。

这个问题纠结了我比较久的时间,由于那个项目用到了ASM8,一开始怀疑是ASM8 对该类做了不当修改,并且花了一些时间来了解 ASM,最终当我分析了字节码,并复现了该问题,我才最终确定了原因。

当我们分别使用 JDK8, JDK9 来编译,产生的字节码比较如下(左边是JDK9的编译结果,右边是JDK8的编译结果)

image-20201117172340308

可以看出,两者的字节码版本都是52.0(即 JDK8),但是 rewind这个方法,在字节码级别上,二者编译结果不同,JDK9上。方法的描述符为 ()Ljava/nio/ByteBuffer;

由于虚拟机最终是根据字节码来进行方法查找的,而 java/nio/ByteBuffer.rewind:()Ljava/nio/ByteBuffer; 意味着,它会去 java/nio/ByteBuffer 类(而不是其父类)中寻找 rewind 方法,且该方法的参数列表为空,返回类型为 Ljava/nio/ByteBuffer

(关于JVM是如何查找和指向一个方法的,参考 https://blog.csdn.net/aha_jasper/article/details/105646684

但是自从JDK9以后,由于 ByteBuffer 重写了 rewind 方法,且重写后返回类型为 ByteBuffer(而不是 Buffer),因此导致了这个问题。

@Override
public ByteBuffer rewind() {
    super.rewind();
    return this;
}

如何解决该问题

由于代码本身就能在 JDK8 上通过编译,因此只需要在找到原因后,对代码进行兼容性优化,就可以解决该问题,针对本文的例子,和那个项目的具体情况,我最终做了下面这样的修改来解决问题

Buffer buffer = ByteBuffer.allocateDirect(14);
((ByteBuffer) buffer).rewind();

结论:

  1. 即使代码同时能在不同版本的JDK上通过编译,也需要尽量避免在高版本JDK编译后拿到低版本JDK运行;
  2. 如果一定要这样做,则需要进行充分测试,并对代码进行兼容性改造。

分类:

技术点:

相关文章: