raphw / byte-buddy

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

`Advice` issue #1708

Closed xuhuanzy closed 2 months ago

xuhuanzy commented 2 months ago

When using Advice, the interceptor onExit is unable to access NewTranslateManager, resulting in the error: Exception in thread "DefaultDispatcher-worker-17 @DocumentationBrowser requests#11981" java.lang.NoClassDefFoundError: cn/yiiguxing/plugin/translate/extensions/NewTranslateManager

Using reflection in onExit fails to retrieve NewTranslateManager, which seems to be a classloader issue. How can I make Advice use the classloader of NewTranslateManager?


class NewTranslateManager : BaseStartupActivity(true, false) {

    override suspend fun onRunActivity(project: Project) {
        try {
            ByteBuddyAgent.install()
            val implKtClass = Class.forName("com.intellij.platform.backend.documentation.impl.ImplKt")
            ByteBuddy()
                .rebase(implKtClass)
                .visit(
                    Advice
                        .to(ComputeDocumentationAdvice::class.java)
                        .on(
                            ElementMatchers.named<MethodDescription>("computeDocumentation")
                        )
                )
                .make()
                .load(implKtClass.classLoader, ClassReloadingStrategy.fromInstalledAgent())
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
    companion object {
        fun processDocument(html: String): String {
            return "$html - Modified";
        }
    }
}

object ComputeDocumentationAdvice {

    @JvmStatic
    @Advice.OnMethodExit
    fun onExit(@Advice.Return result: Any?) {

        val documentationData = result as? DocumentationData ?: return

        val contentField = documentationData.javaClass.getDeclaredField("content")
        contentField.isAccessible = true
        val contentData = contentField.get(documentationData)

        val htmlField = contentData.javaClass.getDeclaredField("html")
        htmlField.isAccessible = true
        val currentHtml = htmlField.get(contentData) as String
        val newHtml = processDocument(currentHtml)
        htmlField.set(contentData, newHtml)
    }
}
raphw commented 2 months ago

Your advice code will be inlined. If the targeted class cannot see all the code in your advice method, you will end up with this exception. Ideally, you avoid calling any external code. Finally, Kotlin creates often multiple classes to implement its code. I recommend using Java for this.

xuhuanzy commented 2 months ago

In the onExit method, the classloader cannot find other classes. Is it possible to specify the class loader for onExit at runtime? Or can I inject the classes I want to call into the classloader of onExit?

raphw commented 2 months ago

You cannot specify the class loader as it is the same as the instrumented class. Ideally, you inject the relevant code, but keep it minimal. You can also inject a dispatcher that calls back the agent, and invoke the relevant code from your agent.

xuhuanzy commented 2 months ago

You can also inject a dispatcher that calls back the agent, and invoke the relevant code from your agent.

How should this be done? Any code examples?

raphw commented 2 months ago

I have given several presentations on this under the title The definitive guide to Java agents: https://assets.ctfassets.net/oxjq45e8ilak/4EFaeFr4kZTjgJF2d3IvRW/9f8b66c722e62386ceb6316bee23473e/Rafael_Winterhalter_The_definite_guide_to_Java_agents.pdf

You find this on youtube, too.

xuhuanzy commented 2 months ago

Thank you for your response. It seems the PDF file is corrupted. image

However, I still don't know how to use the code below to implement the dispatcher. The dispatcher implementation in my class loader cannot be set into the Dispatcher injected into the target class loader, as it indicates they are different classes.

public abstract class Dispatcher {
  public static Dispatcher dispatcher;
  public abstract void doDispatch();
}

But I managed to implement the dispatcher using the code below:

public class CustomDispatcher {
    public static Function<String, String> dispatcher;
}
xuhuanzy commented 2 months ago

Can you share how to inject your own implementation into dispatcher?

public abstract class Dispatcher {
  public static Dispatcher dispatcher;
  public abstract void doDispatch();
}
raphw commented 2 months ago

You would create a custom JAR file that only contains the dispatcher class. This class gets injected into the boot loader.

The boot loader is visible to anybody. Therefore, you can now set the field from your agent and read it from your instrumentation. This way, the two can communicate.

xuhuanzy commented 2 months ago

You would create a custom JAR file that only contains the dispatcher class. This class gets injected into the boot loader.

The boot loader is visible to anybody. Therefore, you can now set the field from your agent and read it from your instrumentation. This way, the two can communicate.

I found the issue. I made sure to load the scheduler in the bootloader first, but after implementing the scheduler in my program, the current class loader loaded the scheduler's abstract class instead of looking for it in the bootloader. Therefore, I could only solve the problem by creating a subclass using ByteBuddy and setting an interceptor for it.

raphw commented 2 months ago

Yes, that's a problem and ideally you do not include the dispatcher in your agent jar, to avoid this.