SoT
SoT

Reputation: 1213

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

Upvotes: 2

Views: 105

Answers (1)

rzwitserloot
rzwitserloot

Reputation: 103608

You are using the 'retransform' / 'reload' functionality for no reason. What you want to do, is inspect all classes as they are loaded and transform them if it is the one you are interested in. If you call reload/retransform/'re' anything, you've messed up. There is no need and it causes the errors you are seeing.

To do this, you call instrumentation.addTransformer in your agentmain (just the one line). The transformer you pass is your own - it is invoked for everything. The first line it should have is 'is this BillPughClass'? If no, just return null which indicates you don't want to transform it.

Otherwise apply your bytecode modification magic and voila.

Upvotes: -1

Related Questions