alibaba/transmittable-thread-local

2.14.3版本在JDK 21中遇到了类转换异常问题,TTL是否支持JDK 21?

brucelwl opened this issue · 13 comments

2.14.3版本是不是还不支持JDK21??

wuwen5 commented

从目前的CI构建中看,是覆盖了JDK21的,覆盖了JDK21环境的测试:

建议你反馈你在jdk21环境中你遇到了什么问题,提供相关有效的信息。

@wuwen5 异常信息如下, 这个问题在Idea中启动ttl 的 agent能够复现, 以命令方式挂载agent启动没有问题, 猜测是和idea的debug agent有冲突

2023-09-28 10:53:34.927 SEVERE [main] TtlTransformer: Fail to transform class sun/net/httpserver/ServerImpl$ReqRspTimeoutTask, cause: java.lang.NullPointerException: Cannot read field "string" because "utf" is null
java.lang.NullPointerException: Cannot read field "string" because "utf" is null
	at com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.ConstPool.getUtf8Info(ConstPool.java:675)
	at com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.MethodParametersAttribute.copy(MethodParametersAttribute.java:90)
	at com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.AttributeInfo.copyAll(AttributeInfo.java:249)
	at com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.MethodInfo.compact(MethodInfo.java:157)
	at com.alibaba.ttl.threadpool.agent.internal.javassist.bytecode.ClassFile.compact(ClassFile.java:242)
	at com.alibaba.ttl.threadpool.agent.internal.javassist.CtClassType.toBytecode(CtClassType.java:1583)
	at com.alibaba.ttl.threadpool.agent.internal.javassist.CtClass.toBytecode(CtClass.java:1519)
	at com.alibaba.ttl.threadpool.agent.TtlTransformer.transform(TtlTransformer.java:81)
	at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:244)
	at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188)
	at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:610)
	at java.base/java.lang.ClassLoader.defineClass2(Native Method)
	at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1118)
	at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:182)
	at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:821)
	at java.base/jdk.internal.loader.BuiltinClassLoader.findClassInModuleOrNull(BuiltinClassLoader.java:741)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:665)
	at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:639)
	at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526)
	at jdk.httpserver/sun.net.httpserver.HttpServerImpl.<init>(HttpServerImpl.java:50)
	at jdk.httpserver/sun.net.httpserver.DefaultHttpServerProvider.createHttpServer(DefaultHttpServerProvider.java:35)
	at jdk.httpserver/com.sun.net.httpserver.HttpServer.create(HttpServer.java:152)
	at jdk.httpserver/com.sun.net.httpserver.HttpServer.create(HttpServer.java:126)
	at io.prometheus.client.exporter.HTTPServer.<init>(HTTPServer.java:144)
	at io.prometheus.client.exporter.HTTPServer.<init>(HTTPServer.java:165)
wuwen5 commented

应该是javassist在处理 sun/net/httpserver/ServerImpl$ReqRspTimeoutTask 字节码的时候出现了问题。

已提交到javassist

wuwen5 commented

知道怎么回事了,问题出现在内部类的字节码发生了细微变化,在java8之后,当使用 -parameters 选项编译Java代码时,编译器会将方法参数的名称存储在类文件的调试信息中。这样,当你使用调试器来调试程序时,可以看到方法的参数名称而不仅仅是它们的类型。

使用-parameters编译时,class信息如下 (当未使用-parameters编译时,不会携带MethodParameters)

public void test(int, java.lang.String);
    descriptor: (ILjava/lang/String;)V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=0, locals=3, args_size=3
         0: return
      LineNumberTable:
        line 4: 0
    MethodParameters:
      Name                           Flags
      i
      s

默认在jdk中的class应该是没有携带-parameters编译的,所以默认不会携带MethodParameters,目前查看class也是这样。
但是在Java21中编译内部类时,即使没携带-parameters编译时,构造函数仍然会携带MethodParameters,这与其他版本jdk字节码不一致,这也是不合理和非预期的地方。

当使用-parameters编译含内部类时,jdk8、jdk17、jdk21对MethodParameters处理一致,如下(有一个参数,参数名称为this$0)

test4.MethodParamInnerClassTest$InnerClass(test4.MethodParamInnerClassTest);
    descriptor: (Ltest4/MethodParamInnerClassTest;)V
    flags: (0x0000)
    Code:
      (略)
    MethodParameters:
        Name                           Flags
        this$0                         final mandated

当未使用-parameters编译含内部类时,jdk8\jdk17如下,没有MethodParameters

test4.MethodParamInnerClassTest$InnerClass(test4.MethodParamInnerClassTest);
    descriptor: (Ltest4/MethodParamInnerClassTest;)V
    flags: (0x0000)
    Code:
      (略)

而java21如下,仍然携带了MethodParameters,表示有一个参数,但无法获取参数名称 (<no name>)。

test4.MethodParamInnerClassTest$InnerClass(test4.MethodParamInnerClassTest);
    descriptor: (Ltest4/MethodParamInnerClassTest;)V
    flags: (0x0000)
    Code:
      (略)
    MethodParameters:
      Name                           Flags
      <no name>                      final mandated

所以,总结为,java21编译的内部类字节码发生了变化,引起的兼容性问题。

javassist在解析java21编译的内部类字节码时,在对MethodParameters字节码的解析时无法从常量池中获取字符信息。
大致可以这么理解,字节码告诉了构造方法有一个参数,参数名对应常量池表的下标为0,常量池表信息从下标1开始,0为jvm保留(具体不详 // index 0 is reserved by the JVM.),即无法从常量池获取字符串信息。

JVM规范:

@wuwen5 是不是意味着即使用jdk8 在添加-parameters参数的情况下,也会触发这个异常?

wuwen5 commented

@wuwen5 是不是意味着即使用jdk8 在添加-parameters参数的情况下,也会触发这个异常?

不会

添加-parameters参数时,无论是java21还是java8都会正确符合预期的带上MethodParameters 信息,此时javassist能正常解析。

现在问题是没使用-parameters参数时,预期是所有方法都不会带MethodParameters,但是java21却在内部类的构造方法中带了没有名称的MethodParameters

@wuwen5 目前看来只能在javassist层面兼容这个问题, 或者反馈给JDK确认是否是预期之类的更改

wuwen5 commented

先记录下可能与之相关的信息

wuwen5 commented

进一步研究发现,影响范围为java21编译出来的非静态的内部类

非静态内部类, 包含MethodParameters <no name>

Java21InnerClassWithoutParameters$InnerClass(Java21InnerClassWithoutParameters);
    descriptor: (LJava21InnerClassWithoutParameters;)V
    flags: (0x0000)
    Code:
      stack=1, locals=2, args_size=2
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 2: 0
    MethodParameters:
      Name                           Flags
      <no name>                      final mandated

静态内部类,不含MethodParameters

Java21InnerClassWithoutParameters$StaticInnerClass();
    descriptor: ()V
    flags: (0x0000)
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
liach commented

虚拟机规范允许MethodParameters里面项目名称指向0,表示参数没有名称。javaassist没有考虑这种情况,因为之前好像没有编译器在里面放0所以实际使用时javaassist没出过错。你可以编译个带Optional参数的enum:

enum TestEnum {
  INSTANCE(Optional.of("A"));
  TestEnum(Optional<String> opt) {}
}

也会受影响,因为编译器加了name ordinal两个参数,类似inner class有个outer this参数。
这个改动主要是为了修这个例子里面的一个bug:本来constructor.getParameters()[2].getParameterizedType()返回是Optional.class,这个改动后因为编译器加信息,会返回代表Optional<String>Type

wuwen5 commented

当前已发布的javassist-3.30.1-GA已经修复了这个问题,但是由于合入了其他修改,编译的字节码升级到了Java 11,因此3.30.1-GA这个版本我们暂时还不能直接升级,需等待下个版本发布.

当前已发布的javassist-3.30.1-GA已经修复了这个问题,但是由于合入了其他修改,编译的字节码升级到了JDK-11

@wuwen5

我也发现了javassist-3.30.1-GA升级到Java 11 😰

看到你在推进让javassist新版继续支持Java 8了 🚀👍

@brucelwl @wuwen5 @liach