Skip to content

Instantly share code, notes, and snippets.

@raphw
Last active March 26, 2024 06:56
Show Gist options
  • Star 23 You must be signed in to star a gist
  • Fork 8 You must be signed in to fork a gist
  • Save raphw/34c0e2fffe2ee7b4f02f to your computer and use it in GitHub Desktop.
Save raphw/34c0e2fffe2ee7b4f02f to your computer and use it in GitHub Desktop.
An example agent that intercepts a method of the bootstrap class loader.
package net.bytebuddy;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatchers;
import java.io.File;
import java.lang.instrument.Instrumentation;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.util.Collections;
import java.util.concurrent.Callable;
public class BootstrapAgent {
public static void main(String[] args) throws Exception {
premain(null, ByteBuddyAgent.install());
HttpURLConnection urlConnection = (HttpURLConnection) new URL("http://www.google.com").openConnection();
System.out.println(urlConnection.getRequestMethod());
}
public static void premain(String arg, Instrumentation inst) throws Exception {
File temp = Files.createTempDirectory("tmp").toFile();
ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, inst).inject(Collections.singletonMap(
new TypeDescription.ForLoadedType(MyInterceptor.class),
ClassFileLocator.ForClassLoader.read(MyInterceptor.class).resolve()));
new AgentBuilder.Default()
.ignore(ElementMatchers.nameStartsWith("net.bytebuddy."))
.enableBootstrapInjection(temp, inst)
.type(ElementMatchers.nameEndsWith(".HttpURLConnection"))
.transform(new AgentBuilder.Transformer() {
@Override
public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader) {
return builder.method(ElementMatchers.named("getRequestMethod")).intercept(MethodDelegation.to(MyInterceptor.class));
}
}).installOn(inst);
}
public static class MyInterceptor {
public static String intercept(@SuperCall Callable<String> zuper) throws Exception {
System.out.println("Intercepted!");
return zuper.call();
}
}
}
@joshgord
Copy link

Hey @raphw, this is exactly the example i needed, thank you! It looks like this gist might be slightly out of date, would these changes reflect what is needed in more recent versions (i.e. 1.10.1)

https://gist.github.com/joshgord/cba34368f95dfcd171f8d4e751336f8e/revisions?diff=unified#diff-731d15b1de06fcf45339a2c724631695

@raphw
Copy link
Author

raphw commented Oct 13, 2019

Yes, this example was created quite early in the agent API's path to maturity, your changes look good to me.

@ni-ze
Copy link

ni-ze commented Dec 15, 2020

Hey @raphw, It does not work when interceptor a loaded jdk class, sush as java.lang.ThreadLocal.set(T value). can you give a example? Thank very much!

@raphw
Copy link
Author

raphw commented Dec 15, 2020

Some JDK classes are already loaded when the agent is installed. In this case, you need to register a RedefinitionStrategy to pick up these classes after they were loaded.

@ni-ze
Copy link

ni-ze commented Dec 16, 2020

eum, I figure it out with @Advice.OnMethodEnter. It happens to meet me.

@raphw
Copy link
Author

raphw commented Dec 16, 2020

Am I understanding you correctly that your problem is solved?

@ni-ze
Copy link

ni-ze commented Dec 17, 2020

yes,I solve it. My code is as follows:

Class<?> target = Class.forName("java.lang.ThreadLocal");
        Class<?> interceptor = Class.forName("com.tencent.cloud.tsw.agent.core.MyAdvice");

        Instrumentation instrumentation = ByteBuddyAgent.install();
        new ByteBuddy()
                .with(Implementation.Context.Disabled.Factory.INSTANCE)
                .redefine(target, ClassFileLocator.ForClassLoader.ofSystemLoader())
                .visit(Advice.to(interceptor).on(ElementMatchers.named("set")))
                .make()
                .load(target.getClassLoader(),  ClassReloadingStrategy.of(instrumentation));

        ThreadLocal<Object> local = new ThreadLocal<>();
        local.set("hello word");
public class MyAdvice {
@Advice.OnMethodEnter
    public static void foo(@Advice.AllArguments(readOnly = false) Object[] wraps) {

       System.out.println("interceptor foo");

        for (Object wrap : wraps) {
            System.out.println(wrap);
        }
    }
}

print:

interceptor foo
hello word

I am not very clear how to do it, but it does work.

could you recommend a few books or papers about how to use bytebuddy, I want to learn more about it, but source code is too hard to read. Thank you.

@raphw
Copy link
Author

raphw commented Dec 17, 2020

There is the tutorial but the documentation needs improvement, indeed.

You would probably want to use .disableClassFormatChanges() rather then .with(Implementation.Context.Disabled.Factory.INSTANCE).

@ni-ze
Copy link

ni-ze commented Dec 18, 2020

Actually, I test this casecase1, but it don't work. so, I use another onecase2.
In my question, threadlocal is also loaded by a separate classloaders, which is bootstrap class loader, before ByteBuddyAgent.install.

I know case1 did work in follow example, but it does not work with threadlocal, maybe id loaded by bootstrap class loader.

public void test(){
        Foo foo = new Foo();
        ByteBuddyAgent.install();

        new AgentBuilder.Default()
                .disableClassFormatChanges()
                .with(AgentBuilder.RedefinitionStrategy.REDEFINITION)
                .type(is(Foo.class))
                .transform(new AgentBuilder.Transformer() {
                    @Override
                    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule module) {
                        return builder.visit(Advice.to(MyAdvice.class).on(named("m")));
                    }
                })
                .installOnByteBuddyAgent();


        foo.m();
    }
class Foo {
    String m() { return "foo"; }
}

print:

interceptor foo

@badabapidas
Copy link

Hi I was trying this sample but with the mentioned version I am getitng a compilation error in the below line
ClassInjector.UsingInstrumentation.Target.BOOTSTRAP

As it seems like Target is private but in the documentation its stil saying its public. So I am confused here. Can you please help me here.

@raphw
Copy link
Author

raphw commented Mar 24, 2021

It's public and it's supposed to work. What does the compiler say?

@badabapidas
Copy link

Compiler is saying that the field can not be found and I can see the target field is private. Please see the below screen shot
image

I am not sure if I have missed anything.

@raphw
Copy link
Author

raphw commented Mar 24, 2021

You are likely referencing the lower-case field target, not the Target class with a capital T.

@badabapidas
Copy link

No I am using the Target with Capital T as given below as given in this agent class example; the code given below

ClassInjector.UsingInstrumentation
.of(tempDirectory, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, instrumentation)
.inject(Collections.singletonMap(new TypeDescription.ForLoadedType(MyInterceptor.class),
ClassFileLocator.ForClassLoader.read(MyInterceptor.class)));

Eclipse is showing Target cannot be resolved or is not a field. Not sure what I am missing.

@badabapidas
Copy link

I can see only couple of options are avaialble while doing ClassInjector.UsingInstrumentation.
image

@raphw
Copy link
Author

raphw commented Mar 24, 2021

It works in my IDE, from any package: ClassInjector.UsingInstrumentation.Target.BOOTSTRAP.

@badabapidas
Copy link

I am using eclipse and facing there. Anyway, @raphw thanks a lot for your time and reply. I appreciate that. Started using bytebuddy so was exploring sample programs. Do we have any tutorial or any git code examples for byte buddy to try out or play around? that will really help all the new users to get familiar with the library easily.

@raphw
Copy link
Author

raphw commented Mar 25, 2021

There's a tutorial on bytebuddy.net. It's slighly outdated but still describes the general idea. Otherwise, there's plenty of material on YouTube.

@wpsadmns
Copy link

wpsadmns commented Aug 5, 2021

nice! it's works good!

@wpsadmns
Copy link

wpsadmns commented Aug 6, 2021

niubi a, da xiong dei!

@i36lib
Copy link

i36lib commented May 19, 2022

Hey @raphw, it's really a good example!

I have meet a problem when I try to intercepts a method of the bootstrap class loader with not bootstrap class, I have reproduce the problem with a demo as below, it seems that the Context class was loaded by the system class loader again at line 110 and cause that it diff from the one at line 73. how can I get the Context class loaded by bootstrap loader at line 110?

I am using byte-buddy and byte-buddy-agent v1.10.22

package me.i36.bytebuddy;

import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.dynamic.loading.ClassInjector;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.SuperCall;
import net.bytebuddy.matcher.ElementMatchers;
import net.bytebuddy.utility.JavaModule;

import java.io.File;
import java.lang.instrument.Instrumentation;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.file.Files;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

public class Main {

    public static void main(String[] args) throws Exception {
        premain(null, ByteBuddyAgent.install());
        HttpURLConnection urlConnection = (HttpURLConnection) new URL("http://www.github.com").openConnection();
        System.out.println(urlConnection.getRequestMethod());

        new SystemClassLoaderClass().testMethod();
    }

    public static void premain(String arg, Instrumentation inst) throws Exception {
        File temp = Files.createTempDirectory("tmp").toFile();
        Map<TypeDescription.ForLoadedType, byte[]> injectMap = new HashMap<>(2);
        injectMap.put(new TypeDescription.ForLoadedType(MyInterceptor.class),
                ClassFileLocator.ForClassLoader.read(MyInterceptor.class));

        // If I don't inject the Context into the bootsrap loader it cause a
        // java.lang.NoClassDefFoundError: me/i36/bytebuddy/Main$Context exception at line 73
        injectMap.put(new TypeDescription.ForLoadedType(Context.class),
                ClassFileLocator.ForClassLoader.read(Context.class));

        ClassInjector.UsingInstrumentation.of(temp, ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, inst).inject(injectMap);
        new AgentBuilder.Default()
                .ignore(ElementMatchers.nameStartsWith("net.bytebuddy."))
                .with(new AgentBuilder.InjectionStrategy.UsingInstrumentation(inst, temp))
                .type(ElementMatchers.nameEndsWith(".HttpURLConnection"))
                .transform(new AgentBuilder.Transformer() {
                    @Override
                    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
                        return builder.method(ElementMatchers.named("getRequestMethod")).intercept(MethodDelegation.to(MyInterceptor.class));
                    }
                })
                // intercepts a method of the system class loader.
                .type(ElementMatchers.named("me.i36.bytebuddy.Main$SystemClassLoaderClass"))
                .transform(new AgentBuilder.Transformer() {
                    @Override
                    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassLoader classLoader, JavaModule javaModule) {
                        return builder.method(ElementMatchers.named("testMethod")).intercept(MethodDelegation.to(SystemClassLoaderClassInterceptor.class));
                    }
                })


                .installOn(inst);
    }

    public static class MyInterceptor {
        private MyInterceptor() {}

        public static String intercept(@SuperCall Callable<String> zuper) throws Exception {
            //Context was loaded by bootstrap class loader
            Context.set("X");                                    // line 73
            System.out.println(Thread.currentThread().getName() +
                    " - Intercepted! set context value to X");

            return zuper.call();
        }
    }

    public static class Context {
        private Context() {}

        private static final ThreadLocal<String> CONTEXT_VALUE = new ThreadLocal<>();

        public static void set(String value) {
            CONTEXT_VALUE.set(value);
        }

        public static String get() {
            return CONTEXT_VALUE.get();
        }

        public static void remove() {
            CONTEXT_VALUE.remove();
        }
    }

    public static class SystemClassLoaderClass {
        public void testMethod() {
            System.out.println(Thread.currentThread().getName() +
                    " - I'm in testMethod");
        }
    }

    public static class SystemClassLoaderClassInterceptor {
        public static String intercept(@SuperCall Callable<String> zuper) throws Exception {
            //Context was loaded by system class loader cause the Context.get() to null
            System.out.println(Thread.currentThread().getName() +
                    " - Intercepted! context value : " + Context.get());    // line 110

            return zuper.call();
        }
    }
}

@raphw
Copy link
Author

raphw commented May 19, 2022

Do you have a stack trace? If you are using later then Java 8, you probably have to add adjustReadEdge to your agent builder API to read the interceptor's module from the boot module as the module system otherwise prevents the access.

@i36lib
Copy link

i36lib commented May 20, 2022

@raphw I'am using jdk1.8.0_51

here is the stack when suspend at line 73, the value of evaluate Context.class.getClassLoader() is null (the bootstrap class loader)

intercept:73, Main$MyInterceptor (me.i36.bytebuddy) [2]
getRequestMethod:-1, HttpURLConnection (java.net)
getRequestMethod$accessor$3jEUJIja:-1, HttpURLConnection (sun.net.www.protocol.http)
call:-1, HttpURLConnection$auxiliary$USarPSaH (sun.net.www.protocol.http)
intercept:77, Main$MyInterceptor (me.i36.bytebuddy) [1]
getRequestMethod:-1, HttpURLConnection (sun.net.www.protocol.http)
main:28, Main (me.i36.bytebuddy)

and below is the stack when suspend at line 110, the value of Context.class.getClassLoader().toString() is sun.misc.Launcher$AppClassLoader@14dad5dc( the system class loader)

intercept:110, Main$SystemClassLoaderClassInterceptor (me.i36.bytebuddy)
testMethod:-1, Main$SystemClassLoaderClass (me.i36.bytebuddy)
main:30, Main (me.i36.bytebuddy)

@raphw
Copy link
Author

raphw commented May 20, 2022

Ah, the Context class must be injected into the boot loader, too. Otherwise, it is not available to boot classes.

@i36lib
Copy link

i36lib commented May 20, 2022

But I have already injected the Context class into the boot loader.

Why can't I get the Context class loaded by the boot loader in the SystemClassLoaderClassInterceptor class ?

(on line 110, the class loader of Context class is not the boot loader)

@i36lib
Copy link

i36lib commented May 21, 2022

Ah, I figure it out, I can get the Context value now, : )

public static class SystemClassLoaderClassInterceptor {
        public static String intercept(@SuperCall Callable<String> zuper) throws Exception {
            Class<?> contextClazz = Class.forName("me.i36.bytebuddy.Main$Context", true, null);
            
            Method method = contextClazz.getMethod("get");
            method.setAccessible(true);

            System.out.println(Thread.currentThread().getName() +
                    " - Intercepted! context value : " + method.invoke(contextClazz));    // the value X

            return zuper.call();
        }
    }

@raphw
Copy link
Author

raphw commented May 22, 2022

You probably have the class included in your agent jar and also attach it to the boot loader. This way it will be loaded in the system loader before it is attached to boot and therefore be loaded twice. With different class loaders, the two classes are different. Try not including it as a ".class" file in the boot loader.

@i36lib
Copy link

i36lib commented Jun 18, 2022

You probably have the class included in your agent jar and also attach it to the boot loader. This way it will be loaded in the system loader before it is attached to boot and therefore be loaded twice. With different class loaders, the two classes are different. Try not including it as a ".class" file in the boot loader.

how can I try not to including it as a ".class" file in the boot loader ?

@raphw
Copy link
Author

raphw commented Jun 19, 2022

Have a look at ClassInjector and Instrumentation which both offer APIs for that.

@i36lib
Copy link

i36lib commented Jun 20, 2022

Have a look at ClassInjector and Instrumentation which both offer APIs for that.

OK, thanks raphw.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment