raphw/byte-buddy

`Advice` issue

Closed this issue · 10 comments

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)
    }
}

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.

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?

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.

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?

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.

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;
}

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

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

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.

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.

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