Search code examples
javastack-overflowbyte-buddyjavaagents

Byte Buddy's method delegation leads to StackOverflowError


I am trying to create an agent that proxies the methods in order to do some logic before and after the method. In order to do this I use Byte Buddy with method delegation.

Agent:

//Agent code before...
private static void instrument(String agentOps, Instrumentation inst) {
    new AgentBuilder.Default().with(new Eager())
            .ignore(ElementMatchers.nameContains("com.dvelopp.agenttest"))
            .or(ElementMatchers.hasAnnotation(ElementMatchers.annotationType(ElementMatchers.nameContains("SpringBootApplication"))))
            .type((ElementMatchers.any()))
            .transform((builder, typeDescription, classLoader, module) -> builder.method(ElementMatchers.any())
                    .intercept(MethodDelegation.to(Interceptor.class)))
            .installOn(inst);
}
//Agent code after...

Interceptor:

public static class Interceptor {

    @RuntimeType
    public static Object intercept(@SuperCall Callable<?> superCall, @SuperMethod Method superMethod, @Origin Method currentMethod,
                                   @AllArguments Object[] args, @This(optional = true) Object me) throws Exception {
        //... logic
        Object call = superCall.call();
        //... logic
        return call;
    }
}

It works just perfect on a simple console app. But, when I have some common libraries in my classpath(e.g. simple Spring Boot project), it doesn't work as expected. It produces different errors, but most of them are connected to unexpected reflection like this:

Exception in thread "main" java.lang.NoClassDefFoundError: sun/reflect/GeneratedMethodAccessor24
at sun.reflect.GeneratedMethodAccessor24.<clinit>(Unknown Source)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at java.lang.Class.newInstance(Class.java:442)
at sun.reflect.MethodAccessorGenerator$1.run(MethodAccessorGenerator.java:403)
at sun.reflect.MethodAccessorGenerator$1.run(MethodAccessorGenerator.java:394)
at java.security.AccessController.doPrivileged(Native Method)
at sun.reflect.MethodAccessorGenerator.generate(MethodAccessorGenerator.java:393)
at sun.reflect.MethodAccessorGenerator.generateMethod(MethodAccessorGenerator.java:75)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:53)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.apache.logging.log4j.status.StatusLogger.<clinit>(StatusLogger.java:85)
at org.apache.logging.log4j.LogManager.<clinit>(LogManager.java:60)
at org.apache.commons.logging.LogAdapter$Log4jLog.<clinit>(LogAdapter.java:135)
at org.apache.commons.logging.LogAdapter$Log4jAdapter.createLog$original$snrPR67N(LogAdapter.java:102)
at org.apache.commons.logging.LogAdapter$Log4jAdapter.createLog$original$snrPR67N$accessor$5P1QZaof(LogAdapter.java)
at org.apache.commons.logging.LogAdapter$Log4jAdapter$auxiliary$pVmPc64S.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:126)
at org.apache.commons.logging.LogAdapter$Log4jAdapter.createLog(LogAdapter.java)
at org.apache.commons.logging.LogAdapter.createLog$original$4Ty3vM8s(LogAdapter.java:79)
at org.apache.commons.logging.LogAdapter.createLog$original$4Ty3vM8s$accessor$z3gv7aJK(LogAdapter.java)
at org.apache.commons.logging.LogAdapter$auxiliary$6cBoraQE.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:126)
at org.apache.commons.logging.LogAdapter.createLog(LogAdapter.java)
at org.apache.commons.logging.LogFactory.getLog$original$MoDHp2B7(LogFactory.java:67)
at org.apache.commons.logging.LogFactory.getLog$original$MoDHp2B7$accessor$VdtqA6Wx(LogFactory.java)
at org.apache.commons.logging.LogFactory$auxiliary$ak9XEBl0.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:126)
at org.apache.commons.logging.LogFactory.getLog(LogFactory.java)
at org.apache.commons.logging.LogFactory.getLog$original$MoDHp2B7(LogFactory.java:59)
at org.apache.commons.logging.LogFactory.getLog$original$MoDHp2B7$accessor$VdtqA6Wx(LogFactory.java)
at org.apache.commons.logging.LogFactory$auxiliary$8JhBdK8k.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:126)
at org.apache.commons.logging.LogFactory.getLog(LogFactory.java)
at org.springframework.boot.SpringApplication.<clinit>(SpringApplication.java:194)
at hello.Application.main(Application.java:15)
Caused by: java.lang.ClassNotFoundException: sun.reflect.GeneratedMethodAccessor24
at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
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)
... 39 more

As I can see, these errors are happening with objects that are proxies:

e.g. org.apache.commons.logging.LogFactory.getLog$original$MoDHp2B7

I tried to debug that deeper and I found out that super method and origin have different classes. The "super" one has a Proxy class.

@SuperMethod Method superMethod, @Origin Method currentMethod

Is there a way to make delegation work with such objects?

Added:

On this Spring Boot 2 example(https://spring.io/guides/gs/spring-boot/) there is a clear StackOverflowError:

java.lang.StackOverflowError: null
at com.dvelopp.agenttest.MethodCallContext.getCaller(MethodCallContext.java:18)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:108)
at org.springframework.core.env.SystemEnvironmentPropertySource.getSource(SystemEnvironmentPropertySource.java)
at org.springframework.core.env.MapPropertySource.getSource(MapPropertySource.java)
at org.springframework.core.env.SystemEnvironmentPropertySource.getSource$accessor$d92Pg8OK(SystemEnvironmentPropertySource.java)
at org.springframework.core.env.SystemEnvironmentPropertySource$auxiliary$ATPz5tSr.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:122)
at org.springframework.core.env.SystemEnvironmentPropertySource.getSource(SystemEnvironmentPropertySource.java)
at org.springframework.core.env.MapPropertySource.getSource(MapPropertySource.java)
at org.springframework.core.env.SystemEnvironmentPropertySource.getSource$accessor$d92Pg8OK(SystemEnvironmentPropertySource.java)
at org.springframework.core.env.SystemEnvironmentPropertySource$auxiliary$ATPz5tSr.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:122)
at org.springframework.core.env.SystemEnvironmentPropertySource.getSource(SystemEnvironmentPropertySource.java)
at org.springframework.core.env.MapPropertySource.getSource(MapPropertySource.java)
at org.springframework.core.env.SystemEnvironmentPropertySource.getSource$accessor$d92Pg8OK(SystemEnvironmentPropertySource.java)
at org.springframework.core.env.SystemEnvironmentPropertySource$auxiliary$ATPz5tSr.call(Unknown Source)
at com.dvelopp.agenttest.Main$Interceptor.intercept(Main.java:122)

Solution

  • It seems you are using wrong annotations, or functionality.

    I'd recommend check more simplest case. For example replace your implementation:

        @RuntimeType
        public static Object intercept(@SuperCall Callable<?> superCall, @SuperMethod Method superMethod, @Origin Method currentMethod,
                                       @AllArguments Object[] args, @This(optional = true) Object me) throws Exception {
            return superCall.call();
        }
    

    with next one:

        @RuntimeType
        public static Object intercept(@RuntimeType Object value) throws Exception {
            return value;
        }
    

    In this case you avoid of recursion call, but possible will get a chance to detect and analyze different issue, that is clear and has relation on your situation.

    Possible you should pay attention on conceptual issue also. You know java developers using Callable object should never use call() directly.

    Following API description for @RuntimeType it uses for mapping from one object to different one. New question appears: why do you need convert callable object? In which type it is going to be converted (are you sure that without Future wrapper you'll have an object there. There could be null that possible leads to your exception, isn't it?)?