Repository: alibaba/transmittable-thread-local (language: java)

Issue:
2.14.3版本在JDK 21中遇到了类转换异常问题，TTL是否支持JDK 21？
2.14.3版本是不是还不支持JDK21??


Discussion / hints:
从目前的CI构建中看，是覆盖了JDK21的，覆盖了JDK21环境的测试：

- #556 

建议你反馈你在jdk21环境中你遇到了什么问题，提供相关有效的信息。
@wuwen5 异常信息如下, 这个问题在Idea中启动ttl 的 agent能够复现, 以命令方式挂载agent启动没有问题, 猜测是和idea的debug agent有冲突

```java
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)
```
应该是`javassist`在处理 `sun/net/httpserver/ServerImpl$ReqRspTimeoutTask` 字节码的时候出现了问题。

已提交到`javassist`：

- issue https://github.com/jboss-javassist/javassist/issues/462
- PR https://github.com/jboss-javassist/javassist/pull/463
知道怎么回事了，问题出现在内部类的字节码发生了细微变化，在java8之后，当使用 `-parameters` 选项编译Java代码时，编译器会将方法参数的名称存储在类文件的调试信息中。这样，当你使用调试器来调试程序时，可以看到方法的参数名称而不仅仅是它们的类型。

使用`-parameters`编译时，class信息如下 (当未使用-parameters编译时，不会携带`MethodParameters`)

```java
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`)

```java
test4.MethodParamInnerClassTest$InnerClass(test4.MethodParamInnerClassTest);
    descriptor: (Ltest4/MethodParamInnerClassTest;)V
    flags: (0x0000)
    Code:
      (略)
    MethodParameters:
        Name                           Flags
        this$0                         final mandated
```

当未使用`-parameters`编译含内部类时，jdk8\jdk17如下，没有`MethodParameters`

```java
test4.MethodParamInnerClassTest$InnerClass(test4.MethodParamInnerClassTest);
    descriptor: (Ltest4/MethodParamInnerClassTest;)V
    flags: (0x0000)
    Code:
      (略)
```

而java21如下，仍然携带了`MethodParameters`，表示有一个参数，但无法获取参数名称 (`<no name>`)。

```java
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规范：

- https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html#jvms-4.7.24
- https://docs.oracle.com/javase/specs/jvms/se21/html/jvms-4.html#jvms-4.7.24



@wuwen5 是不是意味着即使用jdk8 在添加`-parameters`参数的情况下,也会触发这个异常?
> @wuwen5 是不是意味着即使用jdk8 在添加`-parameters`参数的情况下,也会触发这个异常?

不会

添加`-parameters`参数时，无论是java21还是java8都会正确符合预期的带上`MethodParameters `信息，此时`javassist`能正常解析。

现在问题是没使用`-parameters`参数时，预期是所有方法都不会带`MethodParameters`，但是java21却在内部类的构造方法中带了没有名称的`MethodParameters `。


@wuwen5 目前看来只能在`javassist`层面兼容这个问题, 或者反馈给JDK确认是否是预期之类的更改
先记录下可能与之相关的信息

- https://github.com/openjdk/jdk/pull/13167
- https://github.com/openjdk/jdk/pull/9862
- Classfile API throws IOOBE for MethodParameters attribute without parameter names https://bugs.openjdk.org/browse/JDK-8304837

进一步研究发现，影响范围为java21编译出来的非静态的内部类

非静态内部类, 包含`MethodParameters` `<no name>`

```java
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`

```java
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
```


虚拟机规范允许MethodParameters里面项目名称指向0，表示参数没有名称。javaassist没有考虑这种情况，因为之前好像没有编译器在里面放0所以实际使用时javaassist没出过错。你可以编译个带Optional参数的enum：
```java
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`
当前已发布的`javassist-3.30.1-GA`已经修复了这个问题，但是由于合入了其他修改，编译的字节码升级到了`Java 11`，因此`3.30.1-GA`这个版本我们暂时还不能直接升级，需等待下个版本发布.

- https://github.com/jboss-javassist/javassist/pull/473 
- https://github.com/jboss-javassist/javassist/pull/470
> 当前已发布的`javassist-3.30.1-GA`已经修复了这个问题，但是由于合入了其他修改，编译的字节码升级到了`JDK-11`

@wuwen5 

我也发现了`javassist-3.30.1-GA`升级到`Java 11` 😰

看到你在推进让`javassist`新版继续支持`Java 8`了 🚀👍

Fix patch:
diff --git a/pom.xml b/pom.xml
index 6ef45bdb1..c33da8fd8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -255,7 +255,7 @@
 			<dependency>
 				<groupId>org.javassist</groupId>
 				<artifactId>javassist</artifactId>
-				<version>3.29.2-GA</version>
+				<version>3.30.2-GA</version>
 				<optional>true</optional>
 			</dependency>
 			<dependency>


Test patch: