javabytecodejavassistbytecode-manipulation

How to make an existing class (present in jar file) implement Serializable at runtime (by javassist, bytebuddy, etc)?


I have a class in the source code

class BillPughSingleton {

    private BillPughSingleton() {
        System.out.println("BillPughSingleton private constructor called");
    }

    // Lớp nội bộ static chứa instance duy nhất của Singleton
    private static class SingletonHelper {
        private static final BillPughSingleton INSTANCE = new BillPughSingleton();
    }

    public static BillPughSingleton getInstance() {
        return SingletonHelper.INSTANCE;
    }
}

Compiling works fine!!

Now I want to override the jvm bytecode to add serializable capability to this class by using Instrumentation API and Javassist

public class Agent {

  private static final Logger LOGGER = LoggerFactory.getLogger(Agent.class);
  private static Integer count = 0;

  public static void premain(String agentArgs, Instrumentation inst) {
    LOGGER.info("[Agent] In premain method");
    String className = "org.example.genericapplicationeventlistener.BillPughSingleton";
    transformClass2( className, inst );
  }

  private static void transformClass2(String className, Instrumentation inst) {
    System.out.println(count);
    Class<?> targetCls = null;
    ClassLoader targetClassLoader = null;
    // see if we can get the class using forName
    try {
      targetCls = Class.forName(className);
      targetClassLoader = targetCls.getClassLoader();
      transform2(targetCls, targetClassLoader, inst);
      return;
    } catch (Exception ex) {
      LOGGER.error("Class [{}] not found with Class.forName", className);
    }
    // otherwise iterate all loaded classes and find what we want
    for (Class<?> clazz : inst.getAllLoadedClasses()) {
      if (clazz.getName().equals(className)) {
        targetCls = clazz;
        targetClassLoader = targetCls.getClassLoader();
        transform2(targetCls, targetClassLoader, inst);
        return;
      }
    }
    throw new RuntimeException("Failed to find class [" + className + "]");
  }

  private static void transform2(Class<?> targetCls, ClassLoader targetClassLoader, Instrumentation inst) {
    SerializableAdder dt = new SerializableAdder(targetCls.getName(), targetClassLoader);
    inst.addTransformer(dt, true);
    try {
      inst.retransformClasses(targetCls);
    } catch (Exception ex) {
      throw new RuntimeException("Transform failed for class: [" + targetCls.getName() + "]", ex);
    }
  }

  static class SerializableAdder implements ClassFileTransformer {
    private final String targetClassName;
    /** The class loader of the class we want to transform */
    private final ClassLoader targetClassLoader;

    SerializableAdder(String targetClassName, ClassLoader targetClassLoader) {
      this.targetClassName = targetClassName;
      this.targetClassLoader = targetClassLoader;
    }

    @Override
    public byte[] transform(
        ClassLoader loader,
        String className,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) {
      byte[] byteCode = classfileBuffer;

      String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); // replace . with /
      if (!className.equals(finalTargetClassName)) {
        return byteCode;
      }

      if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
        try {
          ClassPool pool = ClassPool.getDefault();
          CtClass ctClass = pool.get(targetClassName);

          if (ctClass.isFrozen()) {
            ctClass.defrost();
          }

          ClassFile classFile = ctClass.getClassFile();
          if ( !Set.of(classFile.getInterfaces()).contains( "java.io.Serializable" ) ) {
            classFile.addInterface( "java.io.Serializable" );

          }
          byteCode = ctClass.toBytecode();

          System.out.println();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }

      return byteCode;
    }
  }
}

return byteCode;

But i got this

Exception in thread "main" java.lang.reflect.InvocationTargetException
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:574)
    at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:491)
    at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:503)
Caused by: java.lang.RuntimeException: Transform failed for class: [org.example.genericapplicationeventlistener.BillPughSingleton]
    at com.aeris.changelog.spring.instrumentation.Agent.transform2(Agent.java:204)
    at com.aeris.changelog.spring.instrumentation.Agent.transformClass2(Agent.java:191)
    at com.aeris.changelog.spring.instrumentation.Agent.premain(Agent.java:101)
    ... 6 more
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to change superclass or interfaces
    at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method)
    at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:169)
    at com.aeris.changelog.spring.instrumentation.Agent.transform2(Agent.java:202)
    ... 8 more
*** java.lang.instrument ASSERTION FAILED ***: "!errorOutstanding" with message Outstanding error when calling method in invokeJavaAgentMainMethod at s\src\java.instrument\share\native\libinstrument\JPLISAgent.c line: 619
*** java.lang.instrument ASSERTION FAILED ***: "success" with message invokeJavaAgentMainMethod failed at s\src\java.instrument\share\native\libinstrument\JPLISAgent.c line: 459
*** java.lang.instrument ASSERTION FAILED ***: "result" with message agent load/premain call failed at s\src\java.instrument\share\native\libinstrument\JPLISAgent.c line: 422

Fatal error: processing of -javaagent failed, processJavaStart failed

This is for learning purpose, so please ignore any stupidity!!

Was I wrong somewhere? Please help!!


UPDATE: Code changed according to rzwitserloot

public class Agent {

  private static final Logger LOGGER = LoggerFactory.getLogger(Agent.class);
  private static Integer count = 0;

  public static void premain(String agentArgs, Instrumentation inst) {
    LOGGER.info("[Agent] In premain method");
    String className = "org.example.genericapplicationeventlistener.BillPughSingleton";
    transformClass2( className, inst );
  }

  private static void transformClass2(String className, Instrumentation inst) {
    Class<?> targetCls = null;
    ClassLoader targetClassLoader = null;
    // see if we can get the class using forName
    try {
      targetCls = Class.forName(className);
      targetClassLoader = targetCls.getClassLoader();
      transform2(targetCls, targetClassLoader, inst);
    } catch (Exception ex) {
      LOGGER.error("Class [{}] not found with Class.forName", className);
      ex.printStackTrace();
    }
  }

  private static void transform2(Class<?> targetCls, ClassLoader targetClassLoader, Instrumentation inst) {
    SerializableAdder dt = new SerializableAdder(targetCls.getName(), targetClassLoader);
    inst.addTransformer(dt, true);
    try {
      inst.retransformClasses(targetCls);
    } catch (Exception ex) {
      throw new RuntimeException("Transform failed for class: [" + targetCls.getName() + "]", ex);
    }
  }

  static class SerializableAdder implements ClassFileTransformer {
    private final String targetClassName;
    /** The class loader of the class we want to transform */
    private final ClassLoader targetClassLoader;

    SerializableAdder(String targetClassName, ClassLoader targetClassLoader) {
      this.targetClassName = targetClassName;
      this.targetClassLoader = targetClassLoader;
    }

    @Override
    public byte[] transform(
        ClassLoader loader,
        String className,
        Class<?> classBeingRedefined,
        ProtectionDomain protectionDomain,
        byte[] classfileBuffer) {
      byte[] byteCode = classfileBuffer;

      String finalTargetClassName = this.targetClassName.replaceAll("\\.", "/"); // replace . with /
      if (!className.equals(finalTargetClassName)) {
        return byteCode;
      }

      if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
        try {
          ClassPool pool = ClassPool.getDefault();
          CtClass ctClass = pool.get(targetClassName);

          if (ctClass.isFrozen()) {
            ctClass.defrost();
          }

          ClassFile classFile = ctClass.getClassFile();
          if ( !Set.of(classFile.getInterfaces()).contains( "java.io.Serializable" ) ) {
            classFile.addInterface( "java.io.Serializable" );

          }
          byteCode = ctClass.toBytecode();

          System.out.println();
        } catch (Exception e) {
          e.printStackTrace();
        }
      }

      return byteCode;
    }
  }
}

In short, removed the for loop. Still the same error!!

Please help


Solution

  • As your error would indicate, you cannot change the interfaces of a class when redefining or retransforming it. In other words, you cannot change the interfaces of a class after it has been loaded, which is what you're currently trying to do in your transform2 method by calling retransformClasses. But you can change the interfaces of a class when it's first loaded.

    That means stop calling retransformClasses. Just add your transformer to the instrumentation object in your agentmain / premain method and you're done. Any filtering can be done inside your transformer. This is what the answer by @rzwitserloot is telling your to do.


    Example

    Here is an example that uses the Class-File API preview feature of Java 23. Note this API is set to become stable in Java 24, which should release 18 March 2025, albeit with some changes.

    This means Java 23 is required to run the example. Or you can modify it to use Javassist instead.

    Project Structure

    <project-directory>
    └───src
        ├───com
        │   └───example
        │           AddSerializableTransformer.java
        │           Agent.java
        │           Foo.java
        │           Main.java
        │
        └───META-INF
                MANIFEST.MF
    

    Source Code

    MANIFEST.MF

    This example bundles the agent with the application in a single executable JAR file. When doing this, the agent is defined by the Launcher-Agent-Class manifest entry.

    Main-Class: com.example.Main
    Launcher-Agent-Class: com.example.Agent
    

    Foo.java

    The class that will be transformed to implement Serializable.

    package com.example;
    
    public class Foo {}
    

    Main.java

    package com.example;
    
    import java.io.Serializable;
    
    public class Main {
      
      public static void main(String[] args) {
        boolean isFooSerializable = Serializable.class.isAssignableFrom(Foo.class);
        System.out.println("Serializable assignable from Foo: " + isFooSerializable);
      }
    }
    

    Agent.java

    Since in this example the agent is defined by the Launcher-Agent-Class manifest entry of an executable JAR file, the agent's entry point is named agentmain (not premain).

    package com.example;
    
    import java.lang.instrument.Instrumentation;
    
    public final class Agent {
    
      public static void agentmain(String agentArgs, Instrumentation inst) {
        var transformer = new AddSerializableTransformer(name -> name.equals("com/example/Foo"));
        inst.addTransformer(transformer);
      }
    }
    

    AddSerializableTransformer.java

    This class is the one that uses the Class-File API.

    package com.example;
    
    import java.lang.classfile.ClassBuilder;
    import java.lang.classfile.ClassElement;
    import java.lang.classfile.ClassFile;
    import java.lang.classfile.ClassModel;
    import java.lang.classfile.Interfaces;
    import java.lang.classfile.constantpool.ClassEntry;
    import java.lang.constant.ClassDesc;
    import java.lang.instrument.ClassFileTransformer;
    import java.lang.instrument.IllegalClassFormatException;
    import java.security.ProtectionDomain;
    import java.util.ArrayList;
    import java.util.Objects;
    import java.util.function.Predicate;
    
    public class AddSerializableTransformer implements ClassFileTransformer {
    
      private final ClassDesc serializableDesc = ClassDesc.of("java.io.Serializable");
      private final Predicate<String> classNameFilter;
    
      public AddSerializableTransformer(Predicate<String> classNameFilter) {
        this.classNameFilter = Objects.requireNonNull(classNameFilter);
      }
    
      @Override
      public byte[] transform(
          ClassLoader loader, 
          String className, 
          Class<?> classBeingRedefined,
          ProtectionDomain protectionDomain, 
          byte[] classfileBuffer) throws IllegalClassFormatException {
        if (!"java/io/Serializable".equals(className) && classNameFilter.test(className)) {
          ClassFile file = ClassFile.of();
          ClassModel model = file.parse(classfileBuffer);
          if (!serializableInInterfaces(model)) {
            return file.transform(model, this::addSerializableInterface);
          }
        }
        return null;
      }
    
      private boolean serializableInInterfaces(ClassModel cm) {
        return cm.interfaces().stream()
            .map(ClassEntry::asSymbol)
            .anyMatch(serializableDesc::equals);
      }
    
      private void addSerializableInterface(ClassBuilder cb, ClassElement e) {
        if (e instanceof Interfaces i) {
          var entries = new ArrayList<ClassEntry>(i.interfaces().size() + 1);
          entries.addAll(i.interfaces());
          entries.add(cb.constantPool().classEntry(serializableDesc));
          cb.with(Interfaces.of(entries));
        } else {
          cb.with(e);
        }
      }
    }
    

    Build & Execute

    Here are the commands to build an executable JAR file from the example and execute it.

    1. Compile:

      javac --enable-preview --release 23 --source-path src -d out/classes src/com/example/*.java
      
    2. Package:

      jar --create --file out/example.jar --manifest src/META-INF/MANIFEST.MF -C out/classes .
      
    3. Execute:

      java --enable-preview -jar out/example.jar
      

    And here is the output:

    Serializable assignable from Foo: true
    

    As you can see, Foo was transformed to implement Serializable.