raphw / byte-buddy

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

Why does ClassNotFoundException appear in Interceptor when running in SpringBoot? #1593

Closed zzhlhc closed 7 months ago

zzhlhc commented 7 months ago

Below is my agent code (target VM running in kubernetes): The AgentMain:

import net.bytebuddy.ByteBuddy;
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.scaffold.TypeValidation;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.pool.TypePool;
import net.bytebuddy.utility.JavaModule;

import java.lang.instrument.ClassDefinition;
import java.lang.instrument.Instrumentation;
import java.lang.instrument.UnmodifiableClassException;

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

public class AgentMain {
     public static void agentmain(String agentArgs, Instrumentation instrumentation) throws Exception {
            ClassLoader springBootClassLoader = ..//get LaunchedURLClassLoader from instrumentation.getAllLoadedClasses();
            ClassLoader oriParent = springBootClassLoader.getParent();

//1. Clear LaunchedURLClassLoader's parent to make sure ApiClientInterceptor is loaded by springBootClassLoader
// Therefore, the classes that ApiClientInterceptor depends on are also loaded from springBootClassLoader.
            clearSpirngCLParent();  

            File jarFile = new File("/backup/OpsAgent-1.0-SNAPSHOT-jar-with-dependencies.jar");
            injectToURLClassLoader(new URL[]{jarFile.toURI().toURL()}, (URLClassLoader) springBootClassLoader);

            addTransformerApiClient(instrumentation, TypePool.Default.of(ClassFileLocator.ForClassLoader.of(springBootClassLoader)));

            //2. Load the Interceptor while springBootClassLoader has no parent loader
            Class<?> apiClientInterceptorClz = springBootClassLoader.loadClass("org.example.ApiClientInterceptor");
            System.out.println("apiClientInterceptorClz:" + apiClientInterceptorClz.getClassLoader());

            restoreSpringCLParent(oriParent);
     }

     private static void addTransformerApiClient(Instrumentation instrumentation, final TypePool typePool) {
        new AgentBuilder.Default()
                .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
                .disableClassFormatChanges()
                .type(named("io.kubernetes.client.openapi.ApiClient"))
                .transform(new AgentBuilder.Transformer() {
                    @Override
                    public DynamicType.Builder<?> transform(DynamicType.Builder<?> builder,
                                                            TypeDescription typeDescription,
                                                            ClassLoader classLoader,
                                                            JavaModule module,
                                                            ProtectionDomain protectionDomain) {
                        System.out.println("transform ApiClient trigger");
                        return builder.method(named("executeAsync").and(takesArguments(3)))
                                .intercept(MethodDelegation.to(typePool.describe("org.example.ApiClientInterceptor").resolve()));
                    }
                }).installOn(instrumentation);
     }

     private static void clearSpirngCLParent()  {
        Field parentF = ClassLoader.class.getDeclaredField("parent");
        parentF.setAccessible(true);
        parentF.set(springCL, null);
     }

     private static void restoreSpringCLParent(ClassLoader oriParent)  {
        Field parentF = ClassLoader.class.getDeclaredField("parent");
        parentF.setAccessible(true);
        parentF.set(springCL, oriParent);
     }
}

ApiClientInterceptor:

import io.kubernetes.client.openapi.ApiCallback;
import io.kubernetes.client.openapi.ApiClient;
import io.kubernetes.client.openapi.ApiException;
import net.bytebuddy.implementation.bind.annotation.Argument;
import net.bytebuddy.implementation.bind.annotation.This;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;

import java.io.IOException;
import java.lang.reflect.Type;

public class ApiClientInterceptor {
    public static <T> void executeAsync(@This ApiClient apiClient, @Argument(0) Call call, @Argument(1) Type returnType, @Argument(2) ApiCallback<T> callback) {

        //3. test if can load
        Class<?> c= springBootClassLoader.loadClass("okhttp3.Callback");
        //it prints "org.springframework.boot.loader.LaunchedURLClassLoader@1698c449"
        System.out.println(c.getClassLoader());

        //4. this line throws ClassNotFoundException
        Callback cb= new Callback() {
             @Override
            public void onFailure(Call call, IOException e) {}
            ...  
        };
    }
}

Step 4's stacktrace:

Exception in thread "kube-pipeline-exec-1" java.lang.NoClassDefFoundError: okhttp3/Callback
    at java.base/java.lang.ClassLoader.defineClass1(Native Method)
    at java.base/java.lang.ClassLoader.defineClass(ClassLoader.java:1017)
    at java.base/java.security.SecureClassLoader.defineClass(SecureClassLoader.java:174)
    at java.base/jdk.internal.loader.BuiltinClassLoader.defineClass(BuiltinClassLoader.java:800)
    at java.base/jdk.internal.loader.BuiltinClassLoader.findClassOnClassPathOrNull(BuiltinClassLoader.java:698)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClassOrNull(BuiltinClassLoader.java:621)
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:579)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:576)
    at org.springframework.boot.loader.LaunchedURLClassLoader.loadClass(LaunchedURLClassLoader.java:151)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    at org.example.ApiClientDelegation.executeAsync(ApiClientDelegation.java:27)
    at io.kubernetes.client.openapi.ApiClient.executeAsync(ApiClient.java)
    at io.kubernetes.client.openapi.apis.CoreV1Api.deleteNamespacedConfigMapAsync(CoreV1Api.java:22661)
    at coopwire.ops.k8scce.plpeline.sink.ConfigMapDeleteSink.accept(ConfigMapDeleteSink.java:25)
    at coopwire.ops.provider.k8s.core.K8PipelineExecutor.doBegin(K8PipelineExecutor.java:72)
    at coopwire.ops.provider.k8s.core.K8PipelineExecutor$2.run(K8PipelineExecutor.java:46)
    at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
    at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
    at java.base/java.lang.Thread.run(Thread.java:829)
Caused by: java.lang.ClassNotFoundException: okhttp3.Callback
    at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:581)
    at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:178)
    at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522)
    ... 20 more

pom.xml:

<dependencies>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy</artifactId>
            <version>1.14.11</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/net.bytebuddy/byte-buddy-agent -->
        <dependency>
            <groupId>net.bytebuddy</groupId>
            <artifactId>byte-buddy-agent</artifactId>
            <version>1.14.11</version>
        </dependency>

        <dependency>
            <groupId>io.kubernetes</groupId>
            <artifactId>client-java</artifactId>
            <version>16.0.0</version>
            <scope>provided</scope>
        </dependency>

    </dependencies>

I am very confused, because i think 3 and 4 do the same thing. Why does 4 throw an exception and 3 succed?

What could be the reason for this, is it related to the fact that Callback is in kotlin language or it‘s creating anonymous class ? Thanks . orz...

zzhlhc commented 7 months ago

The reason for the exception is that the loading of anonymous classes is delegated to AppClassLoader by LaunchedURLClassLoader, which causes jar in jar classes such as Callback to also be loaded from AppClassLoader.

The solution is that I make the delegation fail by making agent.jar invisible to the AppClassLoader, thereby ensuring that both the anonymous class and the Callback are loaded by the LaunchedURLClassLoader.