Transformation triggered by Java agent only executed if writing/flushing System.out/err before
Today I used ASM (version 8.0.1) for the first time and implemented a super simple Java agent which can definalise (remove final
modifier) classes and methods. This works perfectly. It was much easier to implement than in Javassist or ByteBuddy, in both of which I implemented the same thing before in order to compare how it feels to work with those tools.
My agent looks like this:
package de.scrum_master.agent;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.MethodVisitor;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Modifier;
import java.security.ProtectionDomain;
import static org.objectweb.asm.ClassReader.SKIP_DEBUG;
import static org.objectweb.asm.ClassReader.SKIP_FRAMES;
import static org.objectweb.asm.Opcodes.ASM8;
public class MyAgent {
public static void premain(String configFile, Instrumentation instrumentation) {
transform(configFile, instrumentation);
}
private static void transform(String configFile, Instrumentation instrumentation) {
// System.out.println("MyAgent");
// System.out.flush();
// System.err.println("xxx");
instrumentation.addTransformer(new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) {
ClassReader classReader = new ClassReader(classfileBuffer);
// We just change class/method modifiers -> no need to visit all parts of the code
int flags = SKIP_DEBUG | SKIP_FRAMES;
ClassWriter classWriter = new ClassWriter(classReader, flags);
classReader.accept(new RemoveFinalVisitor(classWriter), flags);
return classWriter.toByteArray();
}
});
}
public static class RemoveFinalVisitor extends ClassVisitor {
public RemoveFinalVisitor(ClassVisitor cv) {
super(ASM8, cv);
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
cv.visit(version, access & ~Modifier.FINAL, name, signature, superName, interfaces);
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
return super.visitMethod(access & ~Modifier.FINAL, name, desc, signature, exceptions);
}
}
}
I am creating my agent incl. manifest with Maven in the usual way:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
<configuration>
<archive>
<manifestEntries>
<Agent-Class>de.scrum_master.agent.MyAgent</Agent-Class>
<Premain-Class>de.scrum_master.agent.MyAgent</Premain-Class>
<Can-Redefine-Classes>true</Can-Redefine-Classes>
<Can-Retransform-Classes>true</Can-Retransform-Classes>
<Can-Set-Native-Method-Prefix>true</Can-Set-Native-Method-Prefix>
</manifestEntries>
</archive>
</configuration>
</plugin>
Then I test if definalisation of a JRE class which usually is not loaded before starting an agent works or not:
package de.scrum_master.agent;
import java.lang.reflect.Modifier;
import java.util.UUID;
public class TestApplication {
public static void main(String[] args) {
System.out.println(Modifier.toString(UUID.class.getModifiers()));
}
}
This prints public final
, i.e. the transformation has not been performed. It is the same when testing this with with an application class on the normal classpath (not bootstrap classpath). Now just uncomment any single one of these lines in the agent and rebuild the agent JAR:
// System.out.println("MyAgent");
// System.out.flush();
// System.err.println("xxx");
As long as any standard/error output stream is flushed or written to before calling instrumentation.addTransformer(...)
, this magically makes the ASM transformation work, I have absolutely no idea why. But when running TestApplication
now, the console log says public
, i.e. the final
is gone as expected.