From javax.* to jakarta.*: A Simple Proof of Concept

Now that the Jakarta EE project is planning to release its next version (Jakarta EE 9), where the major change is the update of all its APIs to use jakarta.* instead of javax.* in the package names, and hence the issue of breaking binary compatibility, I decided to experiment a little bit with how code that uses javax.* APIs can be dynamically modified (without the need to recompile) so that it runs against the target jakarta.* namespace. It would also be a good opportunity to learn more about Javassist, which I’ll be using to do the renaming at the bytecode level. Note that this post is not intended to propose a solution to this problem of API compatibility. It simply shares an experimentation related to the subject.

Some background

Due to trademark restrictions imposed on the javax.* namespace, Jakarta EE will rename all of its specifications to use jakarta.* in order to move forward with evolving the platform with features as the cloud native Java platform. A major concern here is backward compatibility for existing applications and frameworks using the javax.* APIs in their code. So far, this concern is not explicitly addressed as part of the Jakarta EE specification.

About Javassist

Since this post deals with dynamic manipulation of bytecode, one popular tool for the job is Javassist. It is a very powerful and well-maintained library for editing class files, and is used by many popular projects in the Java ecosystem. Javassists provides both a high-level and a low-level APIs to manipulate bytecode. The low-level API is more flexible and allows editing the raw bytes of the class file, but requires knowledge of the structure of a class file, following the JVM specification. In the example that follows, the low-level API will be used. If you’re not familiar with the structure of Java bytecode, here’s an article that gives an introduction.

Sample code using `javax.*`

The starting point is some dummy code that uses an API under the javax.* namespace (for example, using a Jakarta EE 8 or Java EE 8 API). Here’s a very simple program:

package example.jakartaee;

import javax.json.JsonString;

public class MyJsonString implements JsonString {

    @Override
    public ValueType getValueType() {
        return ValueType.STRING;
    }
    
    @Override
    public String getString() {
        return "test";
    }
    
    @Override
    public CharSequence getChars() {
        return "test";
    }
}

package example.jakartaee;

import javax.json.JsonString;
import javax.json.JsonValue;

public class JakartaEESample {

    static JsonValue jsonValue = new MyJsonString();

    public static void main(String[] args) {
        JsonValue jsonV = new MyJsonString();
        System.out.println(((JsonString)jsonV).getString());
    }
}

So we have some code that use the JSON Processing API in a very dummy (it doesn’t what it does for the moment – all it matters is that it uses some javax API).

Let’s disassemble the code to see what the bytecode looks like, for example for MyJsonString:

javap -v MyJsonString.class
Classfile MyJsonString.class
  Last modified Nov 2, 2019; size 800 bytes
  MD5 checksum a1f98cde65900b434fd184c4980ea911
  Compiled from "MyJsonString.java"
public class example.jakartaee.MyJsonString implements javax.json.JsonString
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #1                          // example/jakartaee/MyJsonString
  super_class: #3                         // java/lang/Object
  interfaces: 1, fields: 0, methods: 4, attributes: 2
Constant pool:
   #1 = Class              #2             // example/jakartaee/MyJsonString
   #2 = Utf8               example/jakartaee/MyJsonString
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Class              #6             // javax/json/JsonString
   #6 = Utf8               javax/json/JsonString
   #7 = Utf8               
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Methodref          #3.#11         // java/lang/Object."":()V
  #11 = NameAndType        #7:#8          // "":()V
  #12 = Utf8               LineNumberTable
  #13 = Utf8               LocalVariableTable
  #14 = Utf8               this
  #15 = Utf8               Lexample/jakartaee/MyJsonString;
  #16 = Utf8               getValueType
  #17 = Utf8               ()Ljavax/json/JsonValue$ValueType;
  #18 = Fieldref           #19.#21        // javax/json/JsonValue$ValueType.STRING:Ljavax/json/JsonValue$ValueType;
  #19 = Class              #20            // javax/json/JsonValue$ValueType
  #20 = Utf8               javax/json/JsonValue$ValueType
  #21 = NameAndType        #22:#23        // STRING:Ljavax/json/JsonValue$ValueType;
  #22 = Utf8               STRING
  #23 = Utf8               Ljavax/json/JsonValue$ValueType;
  #24 = Utf8               getString
  #25 = Utf8               ()Ljava/lang/String;
  #26 = String             #27            // test
  #27 = Utf8               test
  #28 = Utf8               getChars
  #29 = Utf8               ()Ljava/lang/CharSequence;
  #30 = Utf8               SourceFile
  #31 = Utf8               MyJsonString.java
  #32 = Utf8               InnerClasses
  #33 = Class              #34            // javax/json/JsonValue
  #34 = Utf8               javax/json/JsonValue
  #35 = Utf8               ValueType
{
  public example.jakartaee.MyJsonString();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #10                 // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 5: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lexample/jakartaee/MyJsonString;

  public javax.json.JsonValue$ValueType getValueType();
    descriptor: ()Ljavax/json/JsonValue$ValueType;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: getstatic     #18                 // Field javax/json/JsonValue$ValueType.STRING:Ljavax/json/JsonValue$ValueType;
         3: areturn
      LineNumberTable:
        line 9: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       4     0  this   Lexample/jakartaee/MyJsonString;

  public java.lang.String getString();
    descriptor: ()Ljava/lang/String;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #26                 // String test
         2: areturn
      LineNumberTable:
        line 14: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lexample/jakartaee/MyJsonString;

  public java.lang.CharSequence getChars();
    descriptor: ()Ljava/lang/CharSequence;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc           #26                 // String test
         2: areturn
      LineNumberTable:
        line 19: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lexample/jakartaee/MyJsonString;
}
SourceFile: "MyJsonString.java"
InnerClasses:
  public static final #35= #19 of #33;    // ValueType=class javax/json/JsonValue$ValueType of class javax/json/JsonValue

I’ve shown the full bytecode, but the important thing here is that you can see references to javax.json.JsonString and javax.json.JsonValue.ValueType (a nested enum) in the constant pool, which is the long list of shared constants that are used within method bodies. The constants for these classes are javax/json/JsonString in constant #6, and javax/json/JsonValue$ValueType in constant #20 (this is how the JVM names these types).

There is also another reference to javax.* in the bytecode, specifically in the method public javax.json.JsonValue$ValueType getValueType().

If we want to convert this class to use jakarta.*, we need to do two things:

  1. rename occurrences of javax/json/JsonString and javax/json/JsonValue$ValueType in the constant pool
  2. and change the descriptor of the method getValueType() so that its return type becomes jakarta.json.JsonValue$ValueType.

We can do this using Javassist as follows:

import javassist.*;
import javassist.bytecode.*;


ClassPool classPool = ClassPool.getDefault();

CtClass ctClass = classPool.get("example.jakartaee.MyJsonString");
ClassFile classFile = ctClass.getClassFile();

ConstPool constPool = classFile.getConstPool();
constPool.renameClass("javax/json/JsonString", "jakarta/json/JsonString");
constPool.renameClass("javax/json/JsonValue$ValueType", "jakarta/json/JsonValue$ValueType");

MethodInfo getValueTypeMethod = classFile.getMethod("getValueType");
getValueTypeMethod.setDescriptor("()Ljakarta/json/JsonValue$ValueType;");

// overwrite the class file
classFile.write(new DataOutputStream(new FileOutputStream("MyJsonString.class")));

The ConstPool.renameClass() handles the renaming within the constant pool, while the MethodInfo.setDescriptor() modifies the descriptor of the method so that the return type is renamed. Finally we overwrite the class file (we could also save to a separate file).

After executing the above code, the new MyJsonString.class file has the updated bytecode. You can see that it now has new constants for the jakarta.* class and descriptor names, and the method descriptor is also updated:

...
#36 = Utf8               jakarta/json/JsonString
#37 = Utf8               jakarta/json/JsonValue$ValueType
#38 = Utf8               Ljakarta/json/JsonValue$ValueType;
#39 = Utf8               ()Ljakarta/json/JsonValue$ValueType;

...

public jakarta.json.JsonValue$ValueType getValueType();
  descriptor: ()Ljakarta/json/JsonValue$ValueType;

Let’s look now at the other class file to modify, JakartaEESample.class. In this class, we have a field of type javax.json.JsonValue, and a local variable within the `main` method of the same type. We also do a cast to javax.json.JsonString. Similar to what we did for MyJsonString.class, we can rename these classes in the constant pool using ConstPool.renameClass(), and we still have to modify the descriptor of the field (like we did for the method getValueType() in MyJsonString.class:

CtClass ctClass = classPool.get("example.jakartaee.JakartaEESample");        
ClassFile classFile = ctClass.getClassFile();

ConstPool constPool = classFile.getConstPool();
constPool.renameClass("javax/json/JsonString", "jakarta/json/JsonString");
constPool.renameClass("javax/json/JsonValue", "jakarta/json/JsonValue");

FieldInfo fieldInfo = classFile.getFields().get(0);
fieldInfo.setDescriptor("Ljakarta/json/JsonValue;");

// overwrite the class file
classFile.write(new DataOutputStream(new FileOutputStream("JakartaEESample.class")));

After executing the above code, the bytecode of JakartaEESample.class is updated with new constants for the jakarta.* types (which are in turn referenced in the main method) and with an updated field descriptor:

...
#30 = Class              #47            // jakarta/json/JsonString
#31 = Utf8               javax/json/JsonString
#32 = InterfaceMethodref #30.#33        // jakarta/json/JsonString.getString:()Ljava/lang/String;
...
#47 = Utf8               jakarta/json/JsonString
#48 = Utf8               Ljakarta/json/JsonValue;

static jakarta.json.JsonValue jsonValue;
    descriptor: Ljakarta/json/JsonValue;
    flags: (0x0008) ACC_STATIC

...
public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #10                 // class example/jakartaee/MyJsonString
         3: dup
         4: invokespecial #12                 // Method example/jakartaee/MyJsonString."":()V
         7: astore_1
         8: getstatic     #24                 // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_1
        12: checkcast     #30                 // class jakarta/json/JsonString
        15: invokeinterface #32,  1           // InterfaceMethod jakarta/json/JsonString.getString:()Ljava/lang/String;
        20: invokevirtual #36                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        23: return

Verifying that the updated code runs!

Obviously we want to make sure that the updated class files can run without any JVM error. To do this, since we don’t have the real jakarta.* API released, we can create dummy versions of them:

package jakarta.json;

public interface JsonString {

    String getString();
}

package jakarta.json;

public interface JsonValue {

    public enum ValueType {
        ARRAY,
        OBJECT,
        STRING,
        NUMBER,
        TRUE,
        FALSE,
        NULL
    }
}

and we can run provide these dummy interfaces on the classpath when running the class files that were updated by Javassist.

Conclusion

Using powerful libraries like Javassist, dynamic conversion of javax.* referencing code to the new jakarta.* packaging is achievable. You can view the full code here.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s