transforming class has no effect

1.3k views Asked by At

Based on this tutorial I try to get a java agent to work. https://www.baeldung.com/java-instrumentation#loading-a-java-agent

I do get [Agent] Transforming class TestApplication I have no errors, but I can't see any effect of transforming the class.

Eventually I would like to get both static load and dynamic load to work, but for now I focus on the static way.


public class Static_Agent {

    public static void premain(String agentArgs, Instrumentation inst) {
        String[] tokens = agentArgs.split(";");
        String className = tokens[0];
        String methodName = tokens[1];

        System.out.println(">> "+className);
        System.out.println(">> "+methodName);
        transformClass(className, methodName, inst);
    }



    public static void transformClass(String className, String methodName, Instrumentation instrumentation) {
        Class<?> targetCls = null;
        ClassLoader targetClassLoader = null;
        // see if we can get the class using forName
        try {
            targetCls = Class.forName(className);
            targetClassLoader = targetCls.getClassLoader();
            transform(targetCls, methodName, targetClassLoader, instrumentation);
            return;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        // otherwise iterate all loaded classes and find what we want
        for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
            if(clazz.getName().equals(className)) {
                targetCls = clazz;
                targetClassLoader = targetCls.getClassLoader();
                transform(targetCls, methodName, targetClassLoader, instrumentation);
                return;
            }
        }
        throw new RuntimeException("Failed to find class [" + className + "]");
    }


    public static void transform(Class<?> clazz, String methodName, ClassLoader classLoader, Instrumentation instrumentation) {
        Transformer dt = new Transformer(clazz.getName(), methodName, classLoader);
        instrumentation.addTransformer(dt, true);
        try {
            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Transform failed for class: [" + clazz.getName() + "]", ex);
        }
    }



}
public class Transformer implements ClassFileTransformer {


    /** The internal form class name of the class to transform */
    private String targetClassName;
    /** The class loader of the class we want to transform */
    private ClassLoader targetClassLoader;

    private String targetMethodName;

    public Transformer(String targetClassName, String targetMethodName, ClassLoader targetClassLoader) {
        this.targetClassName = targetClassName;
        this.targetClassLoader = targetClassLoader;
        this.targetMethodName = targetMethodName;
    }

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

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

        if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
            System.out.println("[Agent] Transforming class TestApplication");
            try {

                ClassPool cp = ClassPool.getDefault();
                CtClass cc = cp.get(targetClassName);
                CtMethod m = cc.getDeclaredMethod(targetMethodName);
                m.addLocalVariable("startTime", CtClass.longType);
                m.insertBefore("startTime = System.currentTimeMillis();");

                StringBuilder endBlock = new StringBuilder();

                m.addLocalVariable("endTime", CtClass.longType);
                m.addLocalVariable("opTime", CtClass.longType);
                endBlock.append("endTime = System.currentTimeMillis();");
                endBlock.append("opTime = (endTime-startTime)/1000;");

                endBlock.append("System.out.println(\"[Application] Withdrawal operation completed in:\" + opTime + \" seconds!\");");

                m.insertAfter(endBlock.toString());

                byteCode = cc.toBytecode();
                cc.detach();
            } catch (Exception e) {
                System.out.println("Exception"+e);
            }
        }
        return byteCode;
    }
}
public class TestApplication {

    public static void main(String[] args) {

        try {
            TestApplication.run();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    public static void run() throws Exception {
        System.out.println("--- start ---");

        while (true) {
            test();
            Thread.sleep(4_000);
        }


    }


    static int count = 0;

    public static void test() {
        System.out.println(count++);
    }

}

I launch with:

java -javaagent:static_agent.jar="doeke.application.TestApplication;test" -jar application.jar

In case it helps, the project is here: https://github.com/clankill3r/java_agent

Edit:

In the Transformer.java near the end of the file I use e.printStackTrace(); now.

I get the following error:

[Agent] Transforming class TestApplication javassist.NotFoundException: doeke.application.TestApplication at javassist.ClassPool.get(ClassPool.java:436) at doeke.transformer.Transformer.transform(Transformer.java:48) at java.instrument/java.lang.instrument.ClassFileTransformer.transform(ClassFileTransformer.java:246) at java.instrument/sun.instrument.TransformerManager.transform(TransformerManager.java:188) at java.instrument/sun.instrument.InstrumentationImpl.transform(InstrumentationImpl.java:563) at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses0(Native Method) at java.instrument/sun.instrument.InstrumentationImpl.retransformClasses(InstrumentationImpl.java:167) at doeke.static_agent.Static_Agent.transform(Static_Agent.java:56) at doeke.static_agent.Static_Agent.transformClass(Static_Agent.java:34) at doeke.static_agent.Static_Agent.premain(Static_Agent.java:22) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513) at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525)

--- start ---

0

1

1

There are 1 answers

12
garykwwong On BEST ANSWER

Thanks for raising this question to let me have chance to take a look of Java Instrumentation.

After spending some time to cross check your sample codes and the provided tutorial. The problem is not from the programming codes, but the way how to launch your program.

If you add some loggers to the transform() method in Transformer.java, you will find that the code path is broken after running:

ClassPool cp = ClassPool.getDefault();

And, after replacing the exception catching code in the same method from:

} catch (Exception e) {

to:

} catch (NotFoundException | CannotCompileException | IOException e) {

It would give your more hints as below:

Exception in thread "main" java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Unknown Source)
        at sun.instrument.InstrumentationImpl.loadClassAndStartAgent(Unknown Source)
        at sun.instrument.InstrumentationImpl.loadClassAndCallPremain(Unknown Source)
Caused by: java.lang.NoClassDefFoundError: javassist/NotFoundException
        at doeke.static_agent.Static_Agent.transform(Static_Agent.java:60)
        at doeke.static_agent.Static_Agent.transformClass(Static_Agent.java:40)
        at doeke.static_agent.Static_Agent.premain(Static_Agent.java:28)
        ... 6 more
Caused by: java.lang.ClassNotFoundException: javassist.NotFoundException
        at java.net.URLClassLoader.findClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
        at java.lang.ClassLoader.loadClass(Unknown Source)
        ... 9 more
FATAL ERROR in native method: processing of -javaagent failed

Up to this point, the root cause is more apparent. It is because while launching the program, those javassist relevant classes (e.g. ClassPool, CtClass, CtMethod, etc.) cannot refer to its corresponding libraries during the runtime.

So, the solution is:

  1. assuming you have exported the static_agent.jar in the same "build" folder as of application.jar

  2. all other folder structure remain the same as shown in your provided github

  3. let's "cd" to the build folder in the command console

  4. revising the original program launching script as below

Windows OS:

java -javaagent:static_agent.jar="doeke.application.TestApplication;test" -cp ../libs/javassist-3.12.1.GA.jar;application.jar doeke.application.TestApplication

Unix/Linux OS:

java -javaagent:static_agent.jar="doeke.application.TestApplication;test" -cp ../libs/javassist-3.12.1.GA.jar:application.jar doeke.application.TestApplication

You would finally get your expected result:

[Agent] In premain method.
>> doeke.application.TestApplication
>> test
[Agent] Transforming class
--- start ---
0
[Application] Withdrawal operation completed in:0 seconds!
1
[Application] Withdrawal operation completed in:0 seconds!

EDIT

In addition, let me paste some codes regarding how to insert codes in the middle of a method through javassist.

In case the test() method in TestApplication.java is changed as:

line 30    public static void test() {
line 31        System.out.println(count++);
line 32        
line 33        System.out.println("Last line of test() method");
line 34    }

Assume that we want to add a line between the count and the =========, let's say "This is line separator", which the result would look like:

1 
-- This is line separator -- 
Last line of test() method

Then, in the transform(...) method of Transformer.java, you could add a code line as of below:

m.insertAt(32,"System.out.println(\"-- This is line separator --\");");

which makes it becomes:

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

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

    if (className.equals(finalTargetClassName) && loader.equals(targetClassLoader)) {
        System.out.println("[Agent] Transforming class TestApplication");
        try {
            // Step 1 Preparation
            ClassPool cp = ClassPool.getDefault();
            CtClass cc = cp.get(targetClassName);
            CtMethod m = cc.getDeclaredMethod(targetMethodName);

            // Step 2 Declare variables
            m.addLocalVariable("startTime", CtClass.longType);
            m.addLocalVariable("endTime", CtClass.longType);
            m.addLocalVariable("opTime", CtClass.longType);

            // Step 3 Insertion of extra logics/implementation
            m.insertBefore("startTime = System.currentTimeMillis();");

            m.insertAt(32,"System.out.println(\"-- This is line separator --\");");

            StringBuilder endBlock = new StringBuilder();

            endBlock.append("endTime = System.currentTimeMillis();");
            endBlock.append("opTime = (endTime-startTime)/1000;");
            endBlock.append("System.out.println(\"[Application] Withdrawal operation completed in:\" + opTime + \" seconds!\");");

            m.insertAfter(endBlock.toString());

            // Step 4 Detach from ClassPool and clean up stuff
            byteCode = cc.toBytecode();
            cc.detach();
        } catch (NotFoundException | CannotCompileException | IOException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }

    }
    return byteCode;
}

Finally, would get result like below of printing the code in the middle of a method:

[Agent] In premain method.
className=doeke.application.TestApplication
methodName=test
>> doeke.application.TestApplication
>> test
[Agent] Transforming class TestApplication
--- start ---
0
-- This is line separator --
=========
[Application] Withdrawal operation completed in:0 seconds!
1
-- This is line separator --
=========
[Application] Withdrawal operation completed in:0 seconds!
2
-- This is line separator --
=========
[Application] Withdrawal operation completed in:0 seconds!