ddimitrov
ddimitrov

Reputation: 3343

"duplicate class" error in a Bytebuddy agent applying Advice

I've got some ByteBuddy-based instrumentation that I want to provide both for embedded use and as an agent.

The code goes something like this:

public static void premain(String arguments, Instrumentation instrumentation) {
    installedInPremain = true;
    new AgentBuilder.Default()
            .type(ElementMatchers.named("com.acme.FooBar"))
            .transform((builder, typeDescription, classLoader, module) -> visit(builder))
            .installOn(instrumentation);

}

public static void instrumentOnDemand() {
    ByteBuddyAgent.install();
    DynamicType.Builder<URLPropertyLoader> typeBuilder = new ByteBuddy().redefine(FooBar.class);
    DynamicType.Builder<FooBar> visited = visit(typeBuilder);
    visited.make().load(FooBar.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
}

private static <T> DynamicType.Builder<T> visit(DynamicType.Builder<T> builder) {
    return builder.visit(Advice.to(SnoopLoad.class).on(named("load").and(takesArguments(0))))
                .visit(Advice.to(SnoopOpenStream.class).on(named("openStream")))
                .visit(Advice.to(SnoopPut.class).on(named("put")));
}

Then somewhere else, somebody would do:

new FooBar().load("abc123")

And if I was running with instrumentOnDemand, everything would be fine and dandy, but if I was running the agent with premain, I'd get:

Exception in thread "main" java.lang.LinkageError: loader (instance of  sun/misc/Launcher$AppClassLoader): attempted  duplicate class definition for name: "com/acme/FooBar"
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:331)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at com.acme.FooBarAnalyzer.load(FooBarAnalyzer.java:121)
    at com.acme.FooBarAnalyzer.main(FooBarAnalyzer.java:107)

I would guess that because my advices reference FooBar as @Advice.This parameter, it gets loaded prematurely, but wasn't the whole point of having an agent to be able to redefine these?

Also, how come it worked in the on-demand case? I guess I need to tweak the agent builder, or am I missing a step?

Gah... the perils of copying tutorials without understanding... pointers to documentation are also more than welcome!

Upvotes: 0

Views: 787

Answers (2)

Rafael Winterhalter
Rafael Winterhalter

Reputation: 44032

You can just use the AgentBuilder.Transformer.ForAdvice for both the startup and the dynamic instrumentation. This is normally a better choice as this is also more robust against different class loader constellations. This way you do not need to duplicate your logic.

Upvotes: 1

ddimitrov
ddimitrov

Reputation: 3343

Ok, the solution was to use AgentBuilder.Transformer.ForAdvice(), but that created ugly duplication between premain and visit.

public static void premain(String arguments, Instrumentation instrumentation) {
    installedInPremain = true;
    new AgentBuilder.Default()
            .type(named("com.acme.FooBar"))
//          .transform((builder, typeDescription, classLoader, module) -> visit(builder))

             // CAN'T WE HAVE THIS SHARED???
            .transform(advice("load",           "SnoopLoad"))
            .transform(advice("openStream",     "SnoopOpenStream"))
            .transform(advice("put",            "SnoopPut"))
            .installOn(instrumentation);

}

private static AgentBuilder.Transformer.ForAdvice advice(String name, String snoopLoad) {
    return new AgentBuilder.Transformer.ForAdvice().advice(named(load), "com.acme.PropAnalyzerAgent$" + snoopLoad);
}

private static <T> DynamicType.Builder<T> visit(DynamicType.Builder<T> builder) {
    // CAN'T WE HAVE THIS SHARED???
    return builder.visit(Advice.to(SnoopLoad.class).on(named("load")))
                .visit(Advice.to(SnoopOpenStream.class).on(named("openStream")))
                .visit(Advice.to(SnoopPut.class).on(named("put")));
}


public static void instrumentOnDemand() {
    DynamicType.Builder<FooBar> typeBuilder = new ByteBuddy().redefine(FooBar.class);
    DynamicType.Builder<FooBar> visited = visit(typeBuilder);
    visited.make().load(FooBar.class.getClassLoader(), ClassReloadingStrategy.fromInstalledAgent());
}

Nothing that can't be solved by a layer of indirection, but I'd like to think that it's already handled in nicer way somehow.

Upvotes: 0

Related Questions