Jarek Godwin
Jarek Godwin

Reputation: 61

Dynamic class redefinition at runtime

I have been playing recently with java instrumentation API and a byte buddy. My goal is to change the behavior of an already loaded class. I was able to change the existing method but I`ve failed with adding a completely new one.

First approach:

public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
    System.out.println(("[Agent] In agentmain/premain method"));

    Class<?> clazz = Class.forName("com.example.instrumentation.agent.AppService");

    inst.addTransformer(new AppServiceTransformer(), true);
    inst.retransformClasses(clazz);
}
public class AppServiceTransformer implements ClassFileTransformer {

    @Override
    public byte[] transform(ClassLoader loader, String className, Class<?> classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException {
        byte[] byteCode = null;
        System.out.println("Transformation");
        try {
            byteCode = new ByteBuddy()
                .redefine(classBeingRedefined)
//                .defineMethod("getExperimental", String.class, Opcodes.ACC_PUBLIC)
//                .intercept(FixedValue.value("This is a message from the ByteBuddy hacker !!!"))
                .method(named("getAnswer"))
                .intercept(FixedValue.value("Service has been hacked :)"))
                .make()
                .getBytes();
        } catch (Throwable e) {
            System.err.println(e);
            System.err.println("Failed to transform");
        }
        return byteCode;
    }
}

Above code works, when I attach this agent to an already running VM it alters the behavior of the specified method. However when I uncomment the code responsible for defining a new method what I get is a

Exception in thread "main" com.sun.tools.attach.AgentInitializationException: Agent JAR loaded but agent failed to initialize

I tried running above example as a premain agent loaded at application start-up. For this case altering the behaviour of a the method works but adding a new one throws

Failed to transform
    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:567)
    at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndStartAgent(InstrumentationImpl.java:513)
    at java.instrument/sun.instrument.InstrumentationImpl.loadClassAndCallPremain(InstrumentationImpl.java:525)
Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

Second approach:

    public static void agentmain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println(("[Agent] In agentmain/premain method"));

        new AgentBuilder.Default()
            .type(named("com.jarek.example.instrumentation.agent.AppService"))
            .transform(new AgentBuilder.Transformer() {
                @Override
                public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
                    System.out.println("Entered transform");
                    return builder.method(named("getAnswer"))
                        .intercept(FixedValue.value("Service has been hacked :)"))
                        .defineMethod("getExperimental", String.class, Opcodes.ACC_PUBLIC)
                        .intercept(FixedValue.value("This is experimental feature"));
                }
            })
            .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
            .with(AgentBuilder.TypeStrategy.Default.REDEFINE)
            .installOn(inst);
    }

I can see in the console that the agent have entered the transform method, however the new method isn`t added to the class and the behaviour of the existing one is not altered. Using this solution as a premain agent works perfectly in both cases.

Third approach:

    public static void premain(String agentArgs, Instrumentation inst) throws ClassNotFoundException, UnmodifiableClassException {
        System.out.println(("[Agent] In agentmain/premain method"));

        Class<?> clazz = Class.forName("com.example.instrumentation.agent.AppService");

        ByteBuddyAgent.install();
        new ByteBuddy()
            .redefine(clazz)
//            .defineMethod("getExperimental", String.class, Opcodes.ACC_PUBLIC)
//            .intercept(FixedValue.value("This is a message from the ByteBuddy hacker !!!"))
            .method(named("getAnswer"))
            .intercept(FixedValue.value("Service has been hacked :)"))
            .make()
            .load(clazz.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
}

This case works only for the premain agent for altering an existing method. The attempt to add a new method throws

Caused by: java.lang.UnsupportedOperationException: class redefinition failed: attempted to add a method

When attaching this as a agent to already running application nothing happens.

  1. Does anybody knows if this is possible to achieve what I`ve been trying too ?
  2. Which of those approaches is correct and why do they behave differently ?
  3. How this is it possible that We can change the behaviour of a class which is already loaded to the VM? Has this mechanism some specific name? I've tried to look for some information on the internet but couldn't find anything.
  4. Perhaps some other library i.e. JavaAssist or ASM would be better suited for this case?

Upvotes: 0

Views: 1686

Answers (2)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44042

With an AgentBuilder, you should register a Listener to see if errors happen during retransformation. You probably should set .disableClassFormatChanges() as the average JVM does not support adding methods or fields to a class that already is defined.

Adding a field or method is impossible as it is today, only the code evolution VM supports it as of today and it is doubtful if this feature ever makes it to OpenJDK.

Upvotes: 2

Dhruv Saksena
Dhruv Saksena

Reputation: 219

Since your requirement is to build a framework using which you can collect application metrics. Firstly, there are tools like VisualVM which helps you get metrics from a running java application and see the insights. This will be completely external to your application and won't require any code changes.

If you want a greater control over the metrics, you may onboard Spring Boot Admin and this will do it realtime for you without any code changes. There are plethora of features present in Spring Boot Admin.

Upvotes: -1

Related Questions