JPDA modification listener, some kind of JVMHotRedeployListener

115 views Asked by At

I am hot-deploying code of methods in my development-environment.

The code is affected immediately unless the code change the signature of methods or the structure of the class.

This works fine, I have no problem with that.

Unfortunately I must trigger the execution of the method again in order to have this code executed. Can I register a Listener that get notified when the JVM recieves a JPDA-HotCodeReplace?

1

There are 1 answers

5
apangin On

Whatever IDE/debugger you use, Hot Code Replace eventually ends up in calling either RedefineClasses or RetransformClasses JVM TI function.

So, the idea is to intercept these two functions and call listeners upon their invocation. This can be achieved with a native (JNI) library.

HotCodeReplaceListener.c

#include <jvmti.h>

typedef struct {
    void* unused1[86];
    jvmtiError (JNICALL *RedefineClasses)(jvmtiEnv*, jint, const jvmtiClassDefinition*);
    void* unused2[64];
    jvmtiError (JNICALL *RetransformClasses)(jvmtiEnv*, jint, const jclass*);
} JVMTIFunctions;

jvmtiError (JNICALL *orig_RedefineClasses)(jvmtiEnv*, jint, const jvmtiClassDefinition*);
jvmtiError (JNICALL *orig_RetransformClasses)(jvmtiEnv*, jint, const jclass* classes);

static JavaVM* _vm;

// Call back to static HotCodeReplace.fireListeners() method
static void fire_listeners() {
    JNIEnv* env;
    (*_vm)->GetEnv(_vm, (void**)&env, JNI_VERSION_1_6);

    jclass hot_code_replace = (*env)->FindClass(env, "HotCodeReplace");
    if (hot_code_replace != NULL) {
        jmethodID file_listeners = (*env)->GetStaticMethodID(env, hot_code_replace, "fireListeners", "()V");
        if (file_listeners != NULL) {
            (*env)->CallStaticVoidMethod(env, hot_code_replace, file_listeners);
        }
    }
}

jvmtiError RedefineClassesHook(jvmtiEnv* jvmti, jint class_count, const jvmtiClassDefinition* class_definitions) {
    jvmtiError result = orig_RedefineClasses(jvmti, class_count, class_definitions);
    fire_listeners();
    return result;
}

jvmtiError RetransformClassesHook(jvmtiEnv* jvmti, jint class_count, const jclass* classes) {
    jvmtiError result = orig_RetransformClasses(jvmti, class_count, classes);
    fire_listeners();
    return result;
}

// This function is automatically called when JNI library is loaded
JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved) {
    void* jvmti;
    (*vm)->GetEnv(vm, &jvmti, JVMTI_VERSION_1_0);
    _vm = vm;

    // Change pointers to JVM TI functions, saving their original addresses
    JVMTIFunctions* functions = *(JVMTIFunctions**)jvmti;
    orig_RedefineClasses = functions->RedefineClasses;
    orig_RetransformClasses = functions->RetransformClasses;
    functions->RedefineClasses = RedefineClassesHook;
    functions->RetransformClasses = RetransformClassesHook;

    return JNI_VERSION_1_6;
}

Now in Java code we just need to load this JNI library. fireListeners method will be called from the native code using JNI.

HotCodeReplace.java

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class HotCodeReplace {
    private static final List<Listener> listeners = new CopyOnWriteArrayList<>();

    public static void addListener(Listener listener) {
        listeners.add(listener);
    }

    public static void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    private static void fireListeners() {
        for (Listener listener : listeners) {
            listener.onReplace();
        }
    }

    public interface Listener {
        void onReplace();
    }

    static {
        System.loadLibrary("HotCodeReplaceListener");
    }
}

How to use

HotCodeReplace.addListener(() -> System.out.println("Classes changed!"));

Update

There is no pure Java API to get notifications for class redifinition, except for jdk.ClassRedefinition JFR event appeared in JDK 15:

var rs = new RecordingStream();
rs.enable("jdk.ClassRedefinition");
rs.onEvent("jdk.ClassRedefinition", event -> {
    System.out.println("Classes changed!");
});
rs.startAsync();

The closest JDK 8 alternative is to attach a Java agent that registers dummy ClassFileTransformer to receive callbacks for every loaded or redefined class. The downside of this approach is that the callback fires for every single class, but before the actual redefinition. However, if immediate synchronous notification is not required, you can workaround it with a background timer.

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
import java.security.ProtectionDomain;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class HotCodeReplace {
    private static final List<Listener> listeners = new CopyOnWriteArrayList<>();
    private static final ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);

    public static void addListener(Listener listener) {
        listeners.add(listener);
    }

    public static void removeListener(Listener listener) {
        listeners.remove(listener);
    }

    private static void fireListeners() {
        for (Listener listener : listeners) {
            listener.onReplace();
        }
    }

    public interface Listener {
        void onReplace();
    }

    public static void premain(String args, Instrumentation inst) {
        inst.addTransformer(new ClassFileTransformer() {
            @Override
            public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined,
                                    ProtectionDomain pd, byte[] classFile) {
                if (classBeingRedefined != null) {
                    executor.getQueue().clear();
                    executor.schedule(HotCodeReplace::fireListeners, 1, TimeUnit.SECONDS);
                }
                return null;
            }
        }, true);
    }
}

Note: in order to use Instrumentation API, you'll have to start the JVM with -javaagent option or use Dynamic Attach to attach an agent in runtime.