2

I'm working on an aspectj aspect which needs to know where it's invoked from. At the moment I'm using

new Throwable().getStackTrace();

to access this information but each aspect is taking several hundred microseconds to run.

I've looked at the SecurityManager but that only seems to be able to get me the class name.

Are there any other alternatives I've missed?

Update

JMH Benchmark results referred to in my comment on @apangin's answer:

Benchmark                       Mode  Cnt      Score    Error  Units
MyBenchmark.javalangaccess13i   avgt  100   2025.865 ±  8.133  ns/op
MyBenchmark.javalangaccess2i    avgt  100   2648.598 ± 24.369  ns/op  
MyBenchmark.throwable1          avgt  100  12706.978 ± 84.651  ns/op

Benchmark code:

@Benchmark
public StackTraceElement[] throwable1() {
    return new Throwable().getStackTrace();
}

@SuppressWarnings("restriction")
@Benchmark
public static StackTraceElement javalangaccess2i() {
    Exception e = new Exception();
    return sun.misc.SharedSecrets.getJavaLangAccess().getStackTraceElement(e, 2);
}

@SuppressWarnings("restriction")
@Benchmark
public static StackTraceElement javalangaccess13i() {
    Exception e = new Exception();
    return sun.misc.SharedSecrets.getJavaLangAccess().getStackTraceElement(e, 13);
}

Tests run under Windows 10, JDK 1.8.0_112 on a Dell XPS13 9343 (i5-5200U @ 2.2GHz)

Ian
  • 1,507
  • 3
  • 21
  • 36
  • 1
    The obvious question is why you need this. Getting the stack trace takes a fairly long time for all sorts of reasons, so your best bet is to find an alternative solution to your original problem that doesn't involve reading through the stack trace. – biziclop Oct 25 '16 at 17:36
  • 2
    Getting caller information is slow. Every logging framework has that problem for example. And they all advise you not to turn on such information if you need performance. – zapl Oct 25 '16 at 17:38
  • As the other comments say: what you miss is that you have contradicting requirements that you need to resolve somehow; as you will not find a reasonable, robust way of fulfilling them all. – GhostCat Oct 25 '16 at 17:41
  • 1
    Some detailed information about the performance of obtaining the stack trace like this is contained in https://shipilev.net/blog/2014/exceptional-performance/ - it may not be "helpful" in this regard, because it mainly says "Yep, it's expensive!". But maybe interesting nevertheless. – Marco13 Oct 25 '16 at 17:47
  • Have you looked at InvocationHandler? It might not work with your structure but its worth a shot. – Chad Bingham Oct 25 '16 at 17:50
  • What do you mean by "needs to know where it's invoked from"? I am an AspectJ expert and might be able to help you without using reflection, but I need an example. What does your aspect look like? What does the code targeted by that aspect look like? What should the aspect print? – kriegaex Nov 05 '16 at 11:11

2 Answers2

5

Unfortunately, Throwable.getStackTrace() seems to be the only viable option to get the caller frame in pure Java 8.

However, there is a JDK-specific trick to access just one selected stack frame.
It uses non-standard sun.misc.SharedSecrets API.

public static StackTraceElement getCaller() {
    Exception e = new Exception();
    return sun.misc.SharedSecrets.getJavaLangAccess().getStackTraceElement(e, 2);
}

Here 2 is the index of the required frame.

This works fine until the latest JDK 8, but private API will not be accessible in JDK 9. A good news is that Java 9 will have new standard Stack-Walking API. Here is how to do the same in Java 9.

public static StackWalker.StackFrame getCaller() {
    return StackWalker.getInstance(Collections.emptySet(), 3)
            .walk(s -> s.skip(2).findFirst())
            .orElse(null);
}

The alternative option, that works well for both older and newer versions of Java, is JVMTI GetStackTrace function. It requires linking native code though.

apangin
  • 92,924
  • 10
  • 193
  • 247
  • Many thanks for this very full answer. I've run some tests on your suggestions through JMH and it seems that a Throwable.getStackTrace() takes about 13us whereas your suggested SharedSecrets api takes 2~3us, depending on the item accessed. I was going to test out the Java 9 api as well for completeness but it requires tooling changes to Eclipse to get it working and I'm nervous about breaking that. But the benchmark at https://github.com/pingtimeout/stack-walker-benchmark suggests its slower. – Ian Oct 30 '16 at 14:09
  • It looking like the SharedSecrets api might be rather more variable in its duration than my initial testing indicated. I'm seeing values up to 20us (and one outlier over 600us) for the sum of two calls once I put it in my application. – Ian Nov 01 '16 at 00:40
  • I think I found a much better solution based on AspectJ. – kriegaex Nov 05 '16 at 12:04
2

You are talking about AspectJ. So you do not need any reflection but can just use on-board AspectJ means such as thisEnclosingJoinPointStaticPart.getSignature() in combination with a call() pointcut:

Driver application:

package de.scrum_master.app;

public class Application {
    private static final long NUM_LOOPS = 1000 * 1000;

    public static void main(String[] args) {
        Application application = new Application();

        long startTime = System.nanoTime();
        for (long i = 0; i < NUM_LOOPS; i++)
            application.doSomething();
        System.out.printf(
            "%-40s  |  %8.3f ms%n",
            "AspectJ thisEnclosingJoinPointStaticPart",
            (System.nanoTime() - startTime) / 1.0e6
        );

        startTime = System.nanoTime();
        for (long i = 0; i < NUM_LOOPS; i++)
            application.doSomething2();
        System.out.printf(
            "%-40s  |  %8.3f ms%n",
            "Throwable.getStackTrace",
            (System.nanoTime() - startTime) / 1.0e6
        );

        startTime = System.nanoTime();
        for (long i = 0; i < NUM_LOOPS; i++)
            application.doSomething3();
        System.out.printf(
            "%-40s  |  %8.3f ms%n",
            "SharedSecrets.getJavaLangAccess",
            (System.nanoTime() - startTime) / 1.0e6
        );
    }

    public void doSomething() {}
    public void doSomething2() {}
    public void doSomething3() {}
}

Aspect:

package de.scrum_master.aspect;

import de.scrum_master.app.Application;
import sun.misc.SharedSecrets;

public aspect MyAspect {
    before() : call(* Application.doSomething()) {
        Object o = thisEnclosingJoinPointStaticPart.getSignature();
        //System.out.println(o);
    }

    before() : call(* Application.doSomething2()) {
        Object o = new Throwable().getStackTrace()[1];
        //System.out.println(o);
    }

    before() : call(* Application.doSomething3()) {
        Object o = SharedSecrets.getJavaLangAccess().getStackTraceElement(new Throwable(), 1);
        //System.out.println(o);
    }
}

Console log:

AspectJ thisEnclosingJoinPointStaticPart  |     7,246 ms
Throwable.getStackTrace                   |  1852,895 ms
SharedSecrets.getJavaLangAccess           |  1043,050 ms

As you can see, AspectJ is about 140x faster than the next best reflection-based method.

BTW, if you uncomment the print statements in the aspect, you see these three types of output:

void de.scrum_master.app.Application.main(String[])
de.scrum_master.app.Application.main(Application.java:16)
de.scrum_master.app.Application.main(Application.java:21)

Enjoy!

kriegaex
  • 63,017
  • 15
  • 111
  • 202
  • +1 Good point about AspectJ built-in feature! Unfortunately it has severe limitations. Only static location of the immediate caller is provided. Sometimes you need to look a couple of frames deeper (e.g. when observed method is called from a utility method). It also does not respect reflective calls and calls via method references. – apangin Nov 05 '16 at 14:46
  • Well, you did not mention these restrictions.In normal cases my solution is just blazingly fast. Anyway, it was just a finger exercise for me. I wonder why you actually need the caller info at all. Where is the business value? – kriegaex Nov 05 '16 at 15:21
  • I'm not the asker of original question :) Maybe, OP will be absolutely fine with your solution. I just had a similar problem before, and in my case the interesting caller was about 4 frames below. JVMTI appeared to be the fastest way to get it. – apangin Nov 05 '16 at 15:34
  • Oops, that is true. You are not the OP, my bad. – kriegaex Nov 05 '16 at 16:46