how to retransform a class at runtime

Short Answer

  • Don’t iterate through all the loaded classes from Instrumentation. Instead, just examine the class name passed in to the transformer and if it matches your target class, then transform it. Otherwise, simply return the passed classfileBuffer unmodified.
  • Make the set up calls outside the transformer, (i.e. in your case, do the following from your agent) so initialize your transformer with the class name you’re looking to transform (this will be the inner format so instead of foo.bar.Snafu, you will be looking to match against foo/bar/Snafu. Then add the transformer, call retransform and then remove the transformer.
  • In order to call retransform, you will need the actual [pre-transform] class which you can find by calling Class.forName (in the agentmain), or if you absolutely have to, you could find it in Intrumentation.getAllLoadedClasses() as a last resort. If the target class has not been loaded, you will need the classloader to call Class.forName(name, boolean, classloader) in which case you can pass the URL to the target class class-path in the agent main string args.

Long Answer

Here’s a few recommendations:

  1. Separate out the operation you’re calling into 2 separate operations:
    1. Install the agent. This only needs to be done once.
    2. Transform the target class[es]. You might want to do this n times.
  2. I would implement 1.2 by registering a simple JMX MBean when you install the agent. This MBean should provide an operation like public void transformClass(String className). and should be initialized with a reference to the agent’s acquired Instrumentation instance. The MBean class, interface and any required 3rd party classes should be included in your agent’s loaded.jar. It should also contain your ModifyMethodTest class (which I assume it already does).
  3. At the same time that you install your agent jar, also install the management agent from $JAVA_HOME/lib/management-agent.jar which will activate the management agent so you can invoke the transform operation in the MBean you’re about to register.
  4. Parameterize your DemoTransformer class to accept the internal form of the name of the class you want to transform. (i.e. if your binary class name is foo.bar.Snafu, the internal form will be foo/bar/Snafu. As your DemoTransformer instance starts getting transform callbacks, ignore all class names that do not match the internal form class name you specified. (i.e. simply return the classfileBuffer unmodified)
  5. Your tranformer MBean transformClass operation should then:
    1. Convert the passed class name to internal form.
    2. Create a new DemoTransformer, passing the internal form class name.
    3. Register the DemoTransformer instance using Instrumentation.addTransformer(theNewDemoTransformer, true) .
    4. Call Instrumentation.retransformClasses(ClassForName(className)) (with the binary class name passed to the MBean operation). When this call returns, your class will be transformed.
    5. Remove the transformer with Intrumentation.removeTransformer(theNewDemoTransformer).

The following is an untested approximation of what I mean:

Transformer MBean

public interface TransformerServiceMBean {
    /**
     * Transforms the target class name
     * @param className The binary name of the target class
     */
    public void transformClass(String className);
}

Transformer Service

public class TransformerService implements TransformerServiceMBean {
    /** The JVM's instrumentation instance */
    protected final Instrumentation instrumentation;

    /**
     * Creates a new TransformerService
     * @param instrumentation  The JVM's instrumentation instance 
     */
    public TransformerService(Instrumentation instrumentation) {
        this.instrumentation = instrumentation;
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.shorthandexamples.TransformerServiceMBean#transformClass(java.lang.String)
     */
    @Override
    public void transformClass(String className) {
        Class<?> targetClazz = null;
        ClassLoader targetClassLoader = null;
        // first see if we can locate the class through normal means
        try {
            targetClazz = Class.forName(className);
            targetClassLoader = targetClazz.getClassLoader();
            transform(targetClazz, targetClassLoader);
            return;
        } catch (Exception ex) { /* Nope */ }
        // now try the hard/slow way
        for(Class<?> clazz: instrumentation.getAllLoadedClasses()) {
            if(clazz.getName().equals(className)) {
                targetClazz = clazz;
                targetClassLoader = targetClazz.getClassLoader();
                transform(targetClazz, targetClassLoader);
                return;             
            }
        }
        throw new RuntimeException("Failed to locate class [" + className + "]");
    }

    /**
     * Registers a transformer and executes the transform
     * @param clazz The class to transform
     * @param classLoader The classloader the class was loaded from
     */
    protected void transform(Class<?> clazz, ClassLoader classLoader) {
        DemoTransformer dt = new DemoTransformer(clazz.getName(), classLoader);
        instrumentation.addTransformer(dt, true);
        try {
            instrumentation.retransformClasses(clazz);
        } catch (Exception ex) {
            throw new RuntimeException("Failed to transform [" + clazz.getName() + "]", ex);
        } finally {
            instrumentation.removeTransformer(dt);
        }       
    }
}

The Class Transformer

public class DemoTransformer implements ClassFileTransformer {
    /** The internal form class name of the class to transform */
    protected String className;
    /** The class loader of the class */
    protected ClassLoader classLoader;
    /**
     * Creates a new DemoTransformer
     * @param className The binary class name of the class to transform
     * @param classLoader The class loader of the class
     */
    public DemoTransformer(String className, ClassLoader classLoader) {
        this.className = className.replace('.', "https://stackoverflow.com/");
        this.classLoader = classLoader;
    }

    /**
     * {@inheritDoc}
     * @see java.lang.instrument.ClassFileTransformer#transform(java.lang.ClassLoader, java.lang.String, java.lang.Class, java.security.ProtectionDomain, byte[])
     */
    @Override
    public byte[] transform(ClassLoader loader, String className,
            Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
            byte[] classfileBuffer) throws IllegalClassFormatException {
        if(className.equals(this.className) && loader.equals(classLoader)) {
            return new ModifyMethodTest(classfileBuffer).modiySleepMethod();
        }
        return classfileBuffer;
    }

}

The Agent

public class AgentMain {

    public static void agentmain (String agentArgs, Instrumentation inst) throws Exception {
        TransformerService ts = new TransformerService(inst);
        ObjectName on = new ObjectName("transformer:service=DemoTransformer");
        // Could be a different MBeanServer. If so, pass a JMX Default Domain Name in agentArgs
        MBeanServer server = ManagementFactory.getPlatformMBeanServer();
        server.registerMBean(ts, on);
        // Set this property so the installer knows we're already here
        System.setProperty("demo.agent.installed", "true");     
    }

}

The Agent Installer

public class AgentInstaller {
    /**
     * Installs the loader agent on the target JVM identified in <code>args[0]</code>
     * and then transforms all the classes identified in <code>args[1..n]</code>.
     * @param args The target JVM pid in [0] followed by the classnames to transform
     */
    public static void main(String[] args)  {
        String agentPath = "D:\\work\\workspace\\myjar\\loaded.jar";
        String vid = args[0]; 
        VirtualMachine vm = VirtualMachine.attach(vid);
        // Check to see if transformer agent is installed
        if(!vm.getSystemProperties().contains("demo.agent.installed")) {
            vm.loadAgent(agentPath);  
            // that property will be set now, 
            // and the transformer MBean will be installed
        }
        // Check to see if connector is installed
        String connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null);
        if(connectorAddress==null) {
            // It's not, so install the management agent
            String javaHome = vm.getSystemProperties().getProperty("java.home");
            File managementAgentJarFile = new File(javaHome + File.separator + "lib" + File.separator + "management-agent.jar");
            vm.loadAgent(managementAgentJarFile.getAbsolutePath());
            connectorAddress = vm.getAgentProperties().getProperty("com.sun.management.jmxremote.localConnectorAddress", null);
            // Now it's installed
        }
        // Now connect and transform the classnames provided in the remaining args.
        JMXConnector connector = null;
        try {
            // This is the ObjectName of the MBean registered when loaded.jar was installed.
            ObjectName on = new ObjectName("transformer:service=DemoTransformer");
            // Here we're connecting to the target JVM through the management agent
            connector = JMXConnectorFactory.connect(new JMXServiceURL(connectorAddress));
            MBeanServerConnection server = connector.getMBeanServerConnection();
            for(int i = 1; i < args.length; i++) {
                String className = args[i];
                // Call transformClass on the transformer MBean
                server.invoke(on, "transformClass", new Object[]{className}, new String[]{String.class.getName()});
            }
        } catch (Exception ex) {
            ex.printStackTrace(System.err);
        } finally {
            if(connector!=null) try { connector.close(); } catch (Exception e) {}
        }
        // Done. (Hopefully)
    }
}

================= UPDATE =================

Hey Nick; Yep, that’s one of the limitations of the current (i.e. Java 5-8) class transformers.
To quote from the Instrumentation javadoc:

“The retransformation may change method bodies, the constant pool and
attributes. The retransformation must not add, remove or rename fields
or methods, change the signatures of methods, or change inheritance.
These restrictions maybe be lifted in future versions. The class file
bytes are not checked, verified and installed until after the
transformations have been applied, if the resultant bytes are in error
this method will throw an exception.”

As an aside, this same limitation is documented verbatim for redefining classes too.

As such, you have 2 options:

  1. Don’t add new methods. This is commonly very limiting and disqualifies the use of very common byte-code AOP patterns like method
    wrapping. Depending on which byte-code manipulation library you’re using, you may be able to inject all the functionality you want into
    the existing methods. Some libraries make this easier than others. Or, I should say, some libraries will make this easier than others.

  2. Transform the class before it is class loaded. This uses the same general pattern of the code we already discussed, except you don’t trigger
    the transform through calling retransformClasses. Rather, you register the ClassFileTransformer to perform the transform before the class is loaded
    and your target class will be modified when it is first class loaded. In this case, you’re pretty much free to modify the class any way you
    like, provided the end product can still be validated. Beating the application to the punch (i.e. getting your ClassFileTransformer
    registered before the application loads the class) will most likely require a command like javaagent, although if you have tight control
    of the lifecycle of your application, it is possible to do this in more traditional application layer code. As I said, you just need to make
    sure you get the transformer registered before the target class is loaded.

One other variation on #2 you may be able to use is to simulate a brand new class by using a new classloader. If you create a new
isolated classloader which will not delegate to the existing [loaded] class, but does have access to the [unloaded] target class byte-code,
you’re essentially reproducing the requirements of #2 above since the JVM considers this to be a brand new class.

================ UPDATE ================

In your last comments, I feel like I’ve lost track a bit of where you are. At any rate, Oracle JDK 1.6 most definitely supports retransform. I am not too familiar with ASM, but the last error you posted indicates that the ASM transformation somehow modified the class schema which is not allowed, so the retransform failed.

I figured a working example would add more clarity. The same classes as above (plus one test class called Person) are here. There’s a couple of modifications/additions:

  • The transform operation in the TransformerService now has 3 parameters:
    1. The binary class name
    2. The method name to instrument
    3. A [regular] expression to match the method signature. (if null or empty, matches all signatures)
    4. The actual bytecode modification is done using Javassist in the ModifyMethodTest class. All the instrumentation does is add a System.out.println that looks like this: -->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
  • The AgentInstaller (which has the demo’s Main) just self-installs the agent and the transform service. (Easier for Dev/Demo purposes, but will still work with other JVMs)
  • Once the agent is self-installed, the main thread creates a Person instance and just loops, calling the Person’s two sayHello methods.

Prior to transform, that output looks as follows.

Temp File:c:\temp\com.heliosapm.shorthandexamples.AgentMain8724970986698386534.jar
Installing AgentMain...
AgentMain Installed
Agent Loaded
Instrumentation Deployed:true
Hello [0]
Hello [0]
Hello [1]
Hello [-1]
Hello [2]
Hello [-2]

Person has 2 sayHello methods, one takes an int, the other takes a String. (The String one just prints the negative of the loop index).

Once I start the AgentInstaller, the agent is installed and Person is being invoked in a loop, I connect to the JVM using JConsole:

Finding the AgentInstaller JVM

I navigate to the TransformerService MBean and invoke the transformClass operation. I supply the fully qualified class [binary] name, the method name to instrument, and a regex expression (I)V which matches only the sayHello method which takes an int as an argument. (Or I could supply .*, or nothing to match all overloads). I execute the operation.

Invoking the operation

Now when I go back to the running JVM and examine the output:

Examining class [com/heliosapm/shorthandexamples/Person]
Instrumenting class [com/heliosapm/shorthandexamples/Person]
[ModifyMethodTest] Adding [System.out.println("\n\t-->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]");]
[ModifyMethodTest] Intrumented [1] methods

    -->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
Hello [108]
Hello [-108]

    -->Invoked method [com.heliosapm.shorthandexamples.Person.sayHello((I)V)]
Hello [109]
Hello [-109]

Done. Method instrumented.

Keep in mind, the reason the retransform is allowed is because the Javassist bytecode modification made no changes other than to inject code into an existing method.

Make sense ?

Leave a Comment