raphw / byte-buddy

Runtime code generation for the Java virtual machine.
https://bytebuddy.net
Apache License 2.0
6.27k stars 804 forks source link

Redefine class to replace public static final field value #1015

Closed rnoennig closed 3 years ago

rnoennig commented 3 years ago

I'd like to change a static field's value using byte-buddy in a javaagent: Original class:

package thirdpartylibrary;

public class ExecEnvironment {
    public static final boolean value = false;
}

I'd like to replace this class, so that all access to the field value will evaluate to true. I simulate the thirdpartylibrary with this class:

package thirdpartylibrary;

public class Server {

    Server() {
        System.out.println("Classloader during Instrumentalisation: " +
            ExecEnvironment.class.getClassLoader().getName());
        if (!ExecEnvironment.value) {
            throw new RuntimeException("ExecEnvironment.value still has the orignal value false!");
        }
        System.out.println("~~ > ~~ > ~~ > ~~ > ~~ > ~~ . ~~ < ~~ < ~~ < ~~ < ~~ < ~~ < ");
        System.out.println("                  IT FINALLY WORKED!!!!");
        System.out.println("~~ > ~~ > ~~ > ~~ > ~~ > ~~ . ~~ < ~~ < ~~ < ~~ < ~~ < ~~ < ");
    }

    public static void main(String[] args) {
        new Server();
    }

}

My attempt using byte-buddy:

package agent;

import static net.bytebuddy.matcher.ElementMatchers.named;

import java.lang.instrument.Instrumentation;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.AgentBuilder.Transformer;
import net.bytebuddy.description.modifier.Ownership;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType.Builder;
import net.bytebuddy.implementation.LoadedTypeInitializer;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.JavaModule;

public class Agent {

    public static void premain(String args, Instrumentation instrumentation) throws NoSuchFieldException, SecurityException, Exception {
        System.out.println("---- PREMAIN ----");

        String targetClassName = "thirdpartylibrary.ExecEnvironment";
        String fieldName = "value";
        Class<?> fieldType = Boolean.class;
        boolean newValue = true;

        TypePool typePool = TypePool.Default.ofSystemLoader();
        TypeDescription targetClassType = typePool.describe(targetClassName).resolve();

        new AgentBuilder.Default()
        .with(AgentBuilder.Listener.StreamWriting.toSystemError().withTransformationsOnly())
        .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
        .type(named(targetClassName))
        .transform(new Transformer() {

            @Override
            public Builder<?> transform(Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader,
                    JavaModule module) {
                System.out.println("Modding type: " + typeDescription);
                System.out.println("Classloader during Instrumentalisation: " + classLoader.getName());
                return builder
                        .defineField(fieldName, fieldType, Visibility.PUBLIC, Ownership.STATIC)
                        .initializer(new LoadedTypeInitializer.ForStaticField(fieldName, newValue))
                        ;
            }

        }).installOn(instrumentation);
    }

}

Log output:

---- PREMAIN ----
Modding type: class thirdpartylibrary.ExecEnvironment
Classloader during Instrumentalisation: app
[Byte Buddy] TRANSFORM thirdpartylibrary.ExecEnvironment [jdk.internal.loader.ClassLoaders$AppClassLoader@3d4eac69, unnamed module @a3d8174, loaded=false]
Classloader during Instrumentalisation: app
Exception in thread "main" java.lang.RuntimeException: ExecEnvironment.value still has the orignal value false!
    at thirdpartylibrary.Server.<init>(Server.java:8)
    at thirdpartylibrary.Server.main(Server.java:16)

I build the javaagent via maven and call it using -javaagent:/path/to/TestAgent-0.0.1-SNAPSHOT-jar-with-dependencies.jar in my eclipse run configuration.

I verified via debugging that during transformation and when I access my field that the classloaders have the same ID.

Is it even possible to redefine a class so that the public static field's value has a different value?

It seems like a simple enough use case, but I might be missing some information.

raphw commented 3 years ago

You are out of luck in this particular scenario. A public static final primitive or string value is considered a conpile time constant by javac if assigned directly. It will be resolved to this value by javac anywhere the field is accessed. At rumtime, the fields value does therefore no longer matter as the compile time constant is filled in.

You can validate this by replacing the boolean value by Boolean.parseBoolean("false").

rnoennig commented 3 years ago

Thank you so much for explaining this. Good to know what's going on!

Well, I guess I have to take a look where the field is being accessed and try to modify those methods using advices. Thanks for putting in the work in making this library and giving talks about these topics!

raphw commented 3 years ago

I'm closing this issue, please get in touch if you have any further questions.

domenicosf commented 1 week ago

@raphw how can i access a property of a final class. Any tips? I just need to access the property of a final sub class and intercept a method of a superclass

raphw commented 1 week ago

You need to access it? From the outside? I would use method handles or reflection for this.

domenicosf commented 1 week ago

I just need read access to the field. I need this field to add the value as a header in the superclass method, but the method is in the superclass and the field is in the subclass that is final

raphw commented 1 week ago

You can intercept the method with advice, and make a type check in thaf advice for the subclass. Then cast and access the field from there if the type check applies.