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
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.
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-directory>
└───src
├───com
│ └───example
│ AddSerializableTransformer.java
│ Agent.java
│ Foo.java
│ Main.java
│
└───META-INF
MANIFEST.MF
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);
}
}
}
Here are the commands to build an executable JAR file from the example and execute it.
Compile:
javac --enable-preview --release 23 --source-path src -d out/classes src/com/example/*.java
Package:
jar --create --file out/example.jar --manifest src/META-INF/MANIFEST.MF -C out/classes .
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
.