4

I'm trying to log every call, returned objects and exceptions thrown in methods and constructors using a ByteBuddy (v1.7.9) java agent, without iterfering with the normal functioning of the instrumented code.

My current instantiation of the agent is

new AgentBuilder.Default()
                .with(AgentBuilder.Listener.StreamWriting.toSystemOut())
                .type((typeDescription, classLoader, module, classBeingRedefined, protectionDomain) -> 
                     matcher.matchesIncoming(typeDescription.getTypeName()))
                .transform((builder, typeDescription, classLoader, javaModule) -> builder
                        .visit(Advice.to(CustomAdvicer.class).on(ElementMatchers.any())))
                .installOn(inst);

I have started with the simplest "advicer",

public class CustomAdvicer {
    @Advice.OnMethodEnter
    public static void enter(@Advice.Origin String origin) {
        System.out.println("Entering " + origin);
    }

    @Advice.OnMethodExit(onThrowable = Throwable.class)
    public static void exit(
        @Advice.Return(typing = Assigner.Typing.DYNAMIC) @RuntimeType Object value,
        @Advice.Origin String origin,
        @Advice.Thrown Throwable thrown) {
        System.out.println("Exiting " + origin);
    }
}

However when I run the program I get an exception from bytebuddy:

[Byte Buddy] ERROR some.pack.Thrower [sun.misc.Launcher$AppClassLoader@18b4aac2, null, loaded=false]
java.lang.IllegalStateException: Cannot catch exception during constructor call for public some.pack.Thrower() throws java.lang.Exception
    at net.bytebuddy.asm.Advice.doWrap(Advice.java:515)
    at net.bytebuddy.asm.Advice.wrap(Advice.java:470)
    at net.bytebuddy.asm.AsmVisitorWrapper$ForDeclaredMethods$Entry.wrap(AsmVisitorWrapper.java:481)
    at net.bytebuddy.asm.AsmVisitorWrapper$ForDeclaredMethods$DispatchingVisitor.visitMethod(AsmVisitorWrapper.java:562)
    at net.bytebuddy.jar.asm.ClassVisitor.visitMethod(ClassVisitor.java:327)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining$RedefinitionClassVisitor.visitMethod(TypeWriter.java:3801)
    at net.bytebuddy.jar.asm.ClassReader.readMethod(ClassReader.java:1020)
    at net.bytebuddy.jar.asm.ClassReader.accept(ClassReader.java:698)
    at net.bytebuddy.jar.asm.ClassReader.accept(ClassReader.java:500)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default$ForInlining.create(TypeWriter.java:2941)
    at net.bytebuddy.dynamic.scaffold.TypeWriter$Default.make(TypeWriter.java:1633)
    at net.bytebuddy.dynamic.scaffold.inline.RebaseDynamicTypeBuilder.make(RebaseDynamicTypeBuilder.java:200)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$Transformation$Simple$Resolution.apply(AgentBuilder.java:8905)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.doTransform(AgentBuilder.java:9306)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.transform(AgentBuilder.java:9269)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.access$1300(AgentBuilder.java:9047)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$LegacyVmDispatcher.run(AgentBuilder.java:9625)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer$LegacyVmDispatcher.run(AgentBuilder.java:9575)
    at java.security.AccessController.doPrivileged(Native Method)
    at net.bytebuddy.agent.builder.AgentBuilder$Default$ExecutingTransformer.transform(AgentBuilder.java:9194)
    at sun.instrument.TransformerManager.transform(Unknown Source)
    at sun.instrument.InstrumentationImpl.transform(Unknown Source)
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(Unknown Source)
    at java.security.SecureClassLoader.defineClass(Unknown Source)
    at java.net.URLClassLoader.defineClass(Unknown Source)
    at java.net.URLClassLoader.access$100(Unknown Source)
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.net.URLClassLoader$1.run(Unknown Source)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at sun.misc.Launcher$AppClassLoader.loadClass(Unknown Source)
    at java.lang.ClassLoader.loadClass(Unknown Source)
    at Main.main(Main.java:23)

So, what should I do in order to log the exceptions thrown inside constructors, bearing in mind that it must not interfere with the original code?

By the way, the Thrower class is a silly class I wrote to test this case:

package some.pack;

public class Thrower {
  public Thrower() throws Exception {
    throw new Exception("By courtesy of thrower! ;)");
  }
}
Vampire
  • 35,631
  • 4
  • 76
  • 102
jmmurillo
  • 173
  • 6

1 Answers1

3

The problem is that a constructor has an implicit first instruction which is the invocation of another or a super constructor. Your Thrower class really looks like this:

public class Thrower {
  public Thrower() throws Exception {
    super();
    throw new Exception("By courtesy of thrower! ;)");
  }
}

If you wanted to wrap the entire call in a try-catch block, this would yield this:

public class Thrower {
  public Thrower() throws Exception {
    try {
      super();
      throw new Exception("By courtesy of thrower! ;)");
    } catch (Exception e) {
      ...
    }
  }
}

But this is not legal in the JVM and therefore, Byte Buddy does not allow it. There is neither a good way to exclude the super constructor call as this being the first call is only a Java language convention but byte code allows more arbitrary combinations. As you cannot know what language a class comes from, Byte Buddy does not try any tricks here and simply does not allow it.

Rafael Winterhalter
  • 42,759
  • 13
  • 108
  • 192
  • Is there any alternative way of achieving this goal of logging the exceptions thrown by constructors? – jmmurillo Nov 13 '17 at 13:19
  • I made a related question [here](https://stackoverflow.com/questions/47265835/can-i-instrument-outgoing-method-constructor-calls-with-bytebuddy). Thanks for your quick answer. – jmmurillo Nov 13 '17 at 13:50
  • 2
    Actually, it *is* legal in bytecode (though not in the Java programming language), as long as the exception handler does not try to complete normally, i.e. it has to do either, rethrow the exception, throw another exception or enter an infinite loop. – Holger Nov 13 '17 at 15:09
  • 1
    Indeed, that is good to know, maybe this allows me to add the option to Byte Buddy where the original exception must always be thrown. I am tracking this feature here: https://github.com/raphw/byte-buddy/issues/375 – Rafael Winterhalter Nov 13 '17 at 15:28
  • 1
    I suppose, that’s perfect for the typical use case of injecting a logging action before rethrowing the original exception. – Holger Nov 13 '17 at 15:38
  • @Holger, I don't "speak byte code", so ASM is not my piece of cake. When trying to implement this in Javassist, I ran into a `VerifyError`, see [Javassist issue #325](https://github.com/jboss-javassist/javassist/issues/325). Can you point me to any example where this was successfully implemented using any byte code engineering framework, so at least I have a starting point when further discussing this with maintainers of Javassist, ByteBuddy or ASM? – kriegaex Jun 16 '20 at 08:40
  • 5
    @kriegaex unfortunately, the HotSpot JVM violates the specification more than often and sometimes, like in this case [even deliberately](http://mail.openjdk.java.net/pipermail/hotspot-dev/2017-January/025781.html). By the time I wrote the comment, I had a working example and didn’t know that the authors (or one of them) of the most widespread JVM implementation were about to break it. It still works when lowering the class file version and not using stack maps, but that’s not a viable strategy for the future… – Holger Jun 16 '20 at 11:00
  • Thanks for this very useful comment, @Holger. I wish that other people will upvote it too because it basically kills both my concept and my hope, but I can move on and use another approach. – kriegaex Jun 16 '20 at 13:03