ByteBuffer and the Dreaded NoSuchMethodError
source link: https://www.morling.dev/blog/bytebuffer-and-the-dreaded-nosuchmethoderror/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
ByteBuffer and the Dreaded NoSuchMethodError
The other day, a user in the Debezium community reported an interesting issue;
They were using Debezium with Java 1.8 and got an odd NoSuchMethodError
:
java.lang.NoSuchMethodError: java.nio.ByteBuffer.position(I)Ljava/nio/ByteBuffer;
at io.debezium.connector.postgresql.connection.Lsn.valueOf(Lsn.java:86)
at io.debezium.connector.postgresql.connection.PostgresConnection.tryParseLsn(PostgresConnection.java:270)
at io.debezium.connector.postgresql.connection.PostgresConnection.parseConfirmedFlushLsn(PostgresConnection.java:235)
...
A NoSuchMethodError
typically is an indication for a mismatch of the Java version used to compile some code, and the Java version used for running it:
some method existed at compile time, but it’s not available at runtime.
Now indeed we use JDK 11 for building the Debezium code base, while targeting Java 1.8 as the minimal required version at runtime.
But there is a method position(int)
defined on the Buffer
class
(which ByteBuffer
extends) also in Java 1.8.
And as a matter of fact, the Debezium code compiles just fine with that version, too.
So why would the user run into this error then?
To understand what’s going on, let’s create a very simple class for reproducing the issue:
import java.nio.ByteBuffer;
public class ByteBufferTest {
public static void main(String... args) {
ByteBuffer buffer = ByteBuffer.wrap(new byte[] { 1, 2, 3 });
buffer.position(1); (1)
System.out.println(buffer.get());
}
}
Compile this with a current JDK:
$ javac --source 1.8 --target 1.8 ByteBufferTest.java
And sure enough, the NoSuchMethodError
shows up when running this with Java 1.8:
$ java ByteBufferTest
Exception in thread "main" java.lang.NoSuchMethodError: java.nio.ByteBuffer.position(I)Ljava/nio/ByteBuffer;
at ByteBufferTest.main(ByteBufferTest.java:6)
Whereas, when using 1.8 to compile and run this code, it just works fine.
Now, if we take a closer look at the error message again, the missing method is defined as ByteBuffer position(int)
.
I.e. for an invoked method like position()
, not only its name, parameter type(s), and the name of the declaring class are part of the byte code for that invocation, but also the method’s return type.
A look at the byte code of the class using javap confirms that:
$ javap -p -c -s -v -l -constants ByteBufferTest
...
public static void main(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=4, locals=2, args_size=1
...
19: aload_1
20: iconst_1
21: invokevirtual #13 // Method java/nio/ByteBuffer.position:(I)Ljava/nio/ByteBuffer;
...
And this points us to the right direction;
In Java 1.8, indeed there is no such method, only the position()
method on Buffer
,
which, of course, returns Buffer
and not ByteBuffer
.
Whereas since Java 9, this method (and several others) is overridden in ByteBuffer
— leveraging Java’s support for co-variant return types — to return ByteBuffer
.
The Java compiler will now select that method, ByteBuffer position(int)
, and record that as the invoked method signature in the byte code of the caller class.
This is per-se a nice usability improvement, as it allows to invoke further ByteBuffer
methods on the return value, instead of just those methods declared by Buffer
.
But as we’ve seen, it comes with this little surprise when compiling code on JDK 9 or newer,
while trying to keep compatibility with older Java versions.
And as it turns out, we were not the first or only ones to encounter this issue.
Quite a few open-source projects ran into this, e.g. Eclipse Jetty, Apache Pulsar, Eclipse Vert.x, Apache Thrift, the Yugabyte DB client, and a few others.
How to Prevent This Situation?
So what can you do in order to prevent this issue from happening?
One first idea could be to enforce selection of the right method by casting to Buffer
:
((java.nio.Buffer) buffer).position(1);
But while this produces the desired byte code indeed,
it isn’t exactly the best way for doing so.
You’d have to remember to do so for every invocation of any of the affected ByteBuffer
methods,
and the seemling unneeded cast might be an easy target for some "clean-up" by unsuspecting co-workers on our team.
Luckily, there’s a much better way, and this is to rely on the Java compiler’s --release
parameter,
which was introduced via JEP 247 ("Compile for Older Platform Versions"),
added to the platform also in JDK 9.
In contrast to the more widely known pair of --source
and --target
,
the --release
switch will ensure that only byte code is produced which actually will be useable with the specified Java version.
For this purpose, the JDK contains the signature data for all supported Java versions
(stored in the $JAVA_HOME/lib/ct.sym file).
So all that’s needed really is compiling the code with --release=8
:
$ javac --release=8 ByteBufferTest.java
Examine the bytecode using javap again, and now the expected signature is in place:
21: invokevirtual #13 // Method java/nio/ByteBuffer.position:(I)Ljava/nio/Buffer;
When run on Java 1.8, this virtual method call will be resolved to Buffer#position(int)
at runtime,
whereas on Java 9 and later, it’d resolve to the bridge method inserted by the compiler into the class file of ByteBuffer
due to the co-variant return type, which itself calls the overriding ByteBuffer#position(int)
method.
Now let’s see what happens if we actually try to make use of the overriding method version in ByteBuffer
by re-assigning the result:
...
ByteBuffer buffer = ByteBuffer.wrap(new byte[] { 1, 2, 3 });
buffer = buffer.position(1);
...
Et voilà, this gets rejected by the compiler when targeting Java 1.8,
as the return type of the JDK 1.8 method Buffer#position(int)
cannot be assigned to ByteBuffer
:
$ javac --release=8 ByteBufferTest.java
ByteBufferTest.java:6: error: incompatible types: Buffer cannot be converted to ByteBuffer
buffer = buffer.position(1);
To cut a long story short, we — and many other projects — should have used the --release
switch instead of --source
/--target
, and the user would not have had that issue.
In order to achieve the same in your Maven-based build,
just specify the following property in your pom.xml:
...
<properties>
<maven.compiler.release>8</maven.compiler.release>
</properties>
...
Note that theoretically you could achieve the same effect also when using --source
and --target
;
by means of the --boot-class-path
option, you could advise the compiler to use a specific set of bootstrap class files instead of those from the JDK used for compilation.
But that’d be quite a bit more cumbersome as it requires you to actually provide the classes of the targeted Java version,
whereas --release
will make use of the signature data coming with the currently used JDK itself.
Recommend
-
77
Android 项目开发填坑记 - NoSuchMethodError:(java.lang.System.arraycopy)
-
38
-
35
-
2
java程序报错NoSuchMethodError 作者: wencst 分类: JAVA 发布时间: 2017-11-20 18:06 阅读: 2,889 次 在eclipse中...
-
3
Improve Article ByteBuffer getInt() method in Java with ExamplesLast Updated : 17 Jun, 2019getInt()The getInt() method of ja...
-
4
Improve Article ByteBuffer hashCode() method in Java with ExamplesLast Updated : 17 Jun, 2019The hashCode() method of
-
2
Improve Article ByteBuffer getChar() method in Java with ExamplesLast Updated : 22 Jul, 2019getChar()The getChar() method of
-
2
victoriaMetrics之byteBuffer VictoriaMetrics经常会处理数目庞大的指标,在处理的过程中会涉及指标的拷贝,如果在指标拷贝时都进行内存申请的话,其内存消耗和性能损耗都非常大。victoriaMetrics使用byteBuffer来复用内存,提升性能,其...
-
1
Netty 如何高效接收网络数据?一文聊透 ByteBuffer 动态自适应扩缩容机制 ...
-
3
bytebuffer 方法演示。 引入自写的方法类。 下载 JAVA 文件 netty 包 <dependency> <groupId&g...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK