如何使用堆栈跟踪或反射查找方法的调用者?

442

我需要找到一个方法的调用者。使用stacktrace或反射是否可能实现?


5
只是好奇,但为什么你需要这样做? - Juliet
2
我有一个父类(MVC模型),带有通知事件,只有我的子类的设置器调用此方法。我不想在代码中添加冗余参数。我宁愿让父类中的方法找出调用它的setter。 - Sathish
33
听起来你应该重新考虑那个设计。 - krosenvold
7
作为重构大量代码的一部分,最近我更改了许多东西使用的一个方法。有一种特定的方法来检测代码是否正确地使用了新方法,所以在这些情况下我打印出调用它的类和行号。除了日志记录外,我认为这样做没有真正的目的。尽管我现在想编写API,如果调用方法被命名为foo,则抛出DontNameYourMethodFooException异常。 - Cruncher
8
作为一种不可估量的调试工具,我认为能够获取调用者的方法是非常重要的。这也是我在网上搜索时来到这里的原因。如果我的方法被多个地方调用,那么它是否在正确的时间和位置被调用呢?除了调试或记录日志之外,在其他方面的实用性可能很有限,正如@Cruncher所提到的。 - Ogre Psalm33
显示剩余2条评论
14个回答

456
StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace()

根据Javadocs的说明:

数组的最后一个元素代表堆栈底部,即序列中最近的、最不常见的方法调用。

StackTraceElement有四种方法:getClassName(), getFileName(), getLineNumber()getMethodName()

你需要尝试一下来确定你想要的索引(可能是stackTraceElements[1][2])。


8
我应该指出,getStackTrace() 仍然会创建一个异常,所以这并不是真正更快,只是更方便。 - Michael Myers
49
请注意,这种方法不能提供调用者本身,而只能提供调用者的类型。您将无法引用调用您方法的对象。 - Joachim Sauer
3
仅提一句,但在1.5 JVM上,Thread.currentThread().getStackTrace()似乎比创建一个新的异常慢得多(大约慢3倍)。但如先前所述,无论如何都不应在性能关键区域使用此类代码。 ;) 在1.6 JVM上,速度只慢了约10%,正如Software Monkey所说,这种方式比“new Exception”更能表达意图。 - GaZ
23
Thread.currentThread() 很便宜。Thread.getStackTrace() 很昂贵,因为与 Throwable.fillInStackTrace() 不同,不能保证它被要检查的线程调用,所以 JVM 必须创建一个 "安全点"——锁定堆和栈。参见这个 bug 报告:http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6375302 - David Moles
8
@JoachimSauer 你知道如何获取调用方法的对象引用吗? - jophde
显示剩余6条评论

229
注意:如果您使用的是Java 9或更高版本,应该使用StackWalker.getCallerClass(),如Ali Dehghani的回答中所述。
下面的不同方法的比较主要是出于历史原因而有趣。

此增强请求的评论中可以找到另一种解决方案。 它使用自定义SecurityManagergetClassContext()方法,似乎比堆栈跟踪方法更快。

以下程序测试了不同建议方法的速度(最有趣的部分在内部类SecurityManagerMethod中):

/**
 * Test the speed of various methods for getting the caller class name
 */
public class TestGetCallerClassName {

  /**
   * Abstract class for testing different methods of getting the caller class name
   */
  private static abstract class GetCallerClassNameMethod {
      public abstract String getCallerClassName(int callStackDepth);
      public abstract String getMethodName();
  }

  /**
   * Uses the internal Reflection class
   */
  private static class ReflectionMethod extends GetCallerClassNameMethod {
      public String getCallerClassName(int callStackDepth) {
          return sun.reflect.Reflection.getCallerClass(callStackDepth).getName();
      }

      public String getMethodName() {
          return "Reflection";
      }
  }

  /**
   * Get a stack trace from the current thread
   */
  private static class ThreadStackTraceMethod extends GetCallerClassNameMethod {
      public String  getCallerClassName(int callStackDepth) {
          return Thread.currentThread().getStackTrace()[callStackDepth].getClassName();
      }

      public String getMethodName() {
          return "Current Thread StackTrace";
      }
  }

  /**
   * Get a stack trace from a new Throwable
   */
  private static class ThrowableStackTraceMethod extends GetCallerClassNameMethod {

      public String getCallerClassName(int callStackDepth) {
          return new Throwable().getStackTrace()[callStackDepth].getClassName();
      }

      public String getMethodName() {
          return "Throwable StackTrace";
      }
  }

  /**
   * Use the SecurityManager.getClassContext()
   */
  private static class SecurityManagerMethod extends GetCallerClassNameMethod {
      public String  getCallerClassName(int callStackDepth) {
          return mySecurityManager.getCallerClassName(callStackDepth);
      }

      public String getMethodName() {
          return "SecurityManager";
      }

      /** 
       * A custom security manager that exposes the getClassContext() information
       */
      static class MySecurityManager extends SecurityManager {
          public String getCallerClassName(int callStackDepth) {
              return getClassContext()[callStackDepth].getName();
          }
      }

      private final static MySecurityManager mySecurityManager =
          new MySecurityManager();
  }

  /**
   * Test all four methods
   */
  public static void main(String[] args) {
      testMethod(new ReflectionMethod());
      testMethod(new ThreadStackTraceMethod());
      testMethod(new ThrowableStackTraceMethod());
      testMethod(new SecurityManagerMethod());
  }

  private static void testMethod(GetCallerClassNameMethod method) {
      long startTime = System.nanoTime();
      String className = null;
      for (int i = 0; i < 1000000; i++) {
          className = method.getCallerClassName(2);
      }
      printElapsedTime(method.getMethodName(), startTime);
  }

  private static void printElapsedTime(String title, long startTime) {
      System.out.println(title + ": " + ((double)(System.nanoTime() - startTime))/1000000 + " ms.");
  }
}

我的2.4 GHz 英特尔 Core 2 Duo MacBook 运行 Java 1.6.0_17 的输出示例:
Reflection: 10.195 ms.
Current Thread StackTrace: 5886.964 ms.
Throwable StackTrace: 4700.073 ms.
SecurityManager: 1046.804 ms.

内部反射方法比其他方法要快得多。从新创建的Throwable对象获取堆栈跟踪比从当前线程获取要快。在非内部的查找调用者类的方法中,自定义的SecurityManager似乎是最快的。
更新
正如lyomi在这个评论中指出的那样,sun.reflect.Reflection.getCallerClass()方法在Java 7更新40中默认被禁用,并在Java 8中完全删除。在Java错误数据库中阅读更多关于这个问题的信息。
更新2
正如zammbi发现的那样,Oracle被迫撤回了删除sun.reflect.Reflection.getCallerClass()的更改。它在Java 8中仍然可用(但已被弃用)。
第3次更新
3年后:关于当前JVM的时间更新。
> java -version
java version "1.8.0"
Java(TM) SE Runtime Environment (build 1.8.0-b132)
Java HotSpot(TM) 64-Bit Server VM (build 25.0-b70, mixed mode)
> java TestGetCallerClassName
Reflection: 0.194s.
Current Thread StackTrace: 3.887s.
Throwable StackTrace: 3.173s.
SecurityManager: 0.565s.

5
好的,看起来是这样。但请注意,我在例子中给出的时间是针对一百万次调用的情况 - 所以根据您使用的方式,这可能不是一个问题。 - Johan Kaving
1
对我来说,从我的项目中删除反射导致了10倍的速度提升。 - Kevin Parker
2
是的,一般来说反射是比较慢的(例如参见https://dev59.com/wHRC5IYBdhLWcg3wAcbU),但在这种特定情况下使用内部sun.reflect.Reflection类是最快的。 - Johan Kaving
1
它实际上不需要。您可以通过修改上面的代码以打印返回的className来验证它(我建议将循环次数减少到1)。您会发现所有方法都返回相同的className-TestGetCallerClassName。 - Johan Kaving
1
getCallerClass已被弃用,将在7u40中被移除.. 伤心 :( - lyomi
显示剩余10条评论

57

Java 9 - JEP 259: Stack-Walking API

JEP 259 提供了一种高效的标准 API,用于堆栈遍历并允许轻松地过滤和延迟访问堆栈跟踪中的信息。在 Stack-Walking API 之前,常见的访问堆栈帧的方式包括:

Throwable::getStackTraceThread::getStackTrace 返回一个 StackTraceElement 对象数组,其中包含每个堆栈跟踪元素的类名和方法名。

SecurityManager::getClassContext 是一个受保护的方法,允许 SecurityManager 子类访问类上下文。

JDK-内部的 sun.reflect.Reflection::getCallerClass 方法,无论如何都不应该使用。

通常使用这些 API 是低效的:

这些 API 需要 VM 急切地捕获整个堆栈的快照,并返回表示整个堆栈的信息。如果调用者仅对堆栈顶部的几个帧感兴趣,则无法避免检查所有帧的成本。

为了找到直接调用者的类,首先要获取一个 StackWalker

StackWalker walker = StackWalker
                           .getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE);

然后调用getCallerClass()

Class<?> callerClass = walker.getCallerClass();

或者 walk 一遍 StackFrame 并获取前面的第一个 StackFrame:

walker.walk(frames -> frames
      .map(StackWalker.StackFrame::getDeclaringClass)
      .skip(1)
      .findFirst());

35

听起来你试图避免将this的引用传递到方法中。相比通过当前堆栈跟踪找到调用者,传递this要好得多。 重构为更面向对象的设计甚至更好。你不应该需要知道调用者。必要时可以传递回调对象。


6
知道调用者的信息是过多的信息。如果必须知道,可以传入一个接口,但很可能需要进行重大的重构。@satish应该发布他的代码,让我们来一些有趣的玩意 :) - Bill K
19
有合理的理由想要这样做是存在的。例如,在测试过程中,我曾经发现这很有帮助。 - Eelco
2
@chillenious 我知道 :) 我自己也做过类似的方法,像LoggerFactory.getLogger(MyClass.class)这样,我不需要传入类字面量。但这种做法仍然很少是正确的选择。 - Craig P. Motlin
8
这是一般而言的好建议,但它并没有回答这个问题。 - Navin
1
一个具体的例子,当获取调用者信息是正确的设计决策时,就是在实现.NET INotifyPropertyChanged接口时。虽然这个特定的例子不是针对Java的,但是当尝试将字段/获取器建模为反射的字符串时,同样会出现这个问题。 - Chris Kerekes

24

简洁表述:

Thread.currentThread().getStackTrace()[2].getMethodName()

请注意您可能需要将2替换为1。


5
对于 Android 的小修正:Thread.currentThread().getStackTrace()[3].getMethodName() 返回调用者的方法名。 - Qamar

10

这个方法做的事情与之前相同,但更简单,可能更高效,并且在使用反射时会自动跳过那些帧。唯一的问题是它可能不存在于非Sun JVM中,尽管它已经包含在JRockit 1.4-->1.6的运行时类中。(重点是,它不是一个公共类。)

sun.reflect.Reflection

    /** Returns the class of the method <code>realFramesToSkip</code>
        frames up the stack (zero-based), ignoring frames associated
        with java.lang.reflect.Method.invoke() and its implementation.
        The first frame is that associated with this method, so
        <code>getCallerClass(0)</code> returns the Class object for
        sun.reflect.Reflection. Frames associated with
        java.lang.reflect.Method.invoke() and its implementation are
        completely ignored and do not count toward the number of "real"
        frames skipped. */
    public static native Class getCallerClass(int realFramesToSkip);

关于realFramesToSkip值应该是多少,对于Sun 1.5和1.6 VM版本的java.lang.System来说,有一个叫做getCallerClass()的包保护方法调用了sun.reflect.Reflection.getCallerClass(3),但在我的辅助工具类中我使用了4,因为还有辅助类调用的额外帧。


17
使用JVM实现类是一个非常糟糕的想法。 - Lawrence Dol
7
已注意。我确实指明它不是一个公共类,并且java.lang.System中的受保护方法getCallerClass()在我查看过的所有1.5+虚拟机中都存在,包括IBM、JRockit和Sun,但您的说法保守地很有道理。 - Nicholas
6
@软件猴子,和往常一样,“一切都要看情况”。如果做这样的事情来帮助调试或测试日志记录——特别是如果它永远不会出现在生产代码中——或者部署目标严格是开发人员的个人电脑,那么可能是可以的。对于那些即使在这种情况下仍然持不同意见的人:你需要更好地解释为什么这样做是“真的很糟糕”,而不只是说它很糟糕... - user796820
8
同样的逻辑也可以推导出,每当你使用一个不与JPA兼容的Hibernate特有功能时,那就总是一个“非常糟糕的想法”。或者,如果你要使用仅在其他数据库中不可用的Oracle特定功能,那么这也是一个“非常糟糕的想法”。当然,这是一种更安全的心态,并且对于某些用途来说绝对是好建议,但是仅仅因为它不能与你完全没有使用的软件配置配合工作就自动放弃有用的工具?这有点过于死板并且有点傻。 - user796820
@Perce 对于即时解决方案来说,它运作良好。但如果这些解决方案因任何原因渗入到生产代码中,并且最终需求发生变化(例如在Azul或Google应用引擎上运行),那么维护人员将面临严重问题,如果代码无法运行。供应商特定的类是时间炸弹。 - Thorbjørn Ravn Andersen
5
如果随意使用供应商特定的类,可能会增加出现问题的可能性,但如果所涉及的类不存在(或因某些原因被禁止),则应确定优雅降级的路径。在我看来,完全拒绝使用任何供应商特定的类有点天真。请在一些生产中使用的库的源代码中研究一下,看看它们是否这样做。(例如 sun.misc.Unsafe?) - Nicholas

9
     /**
       * Get the method name for a depth in call stack. <br />
       * Utility function
       * @param depth depth in the call stack (0 means current method, 1 means call method, ...)
       * @return method name
       */
      public static String getMethodName(final int depth)
      {
        final StackTraceElement[] ste = new Throwable().getStackTrace();

        //System. out.println(ste[ste.length-depth].getClassName()+"#"+ste[ste.length-depth].getMethodName());
        return ste[ste.length - depth].getMethodName();
      }

例如,如果您为了调试目的而尝试获取调用方法的行号,则需要越过Utility类,在其中编写这些静态方法:
(旧的java1.4代码,仅用于说明潜在的StackTraceElement使用)
        /**
          * Returns the first "[class#method(line)]: " of the first class not equal to "StackTraceUtils". <br />
          * From the Stack Trace.
          * @return "[class#method(line)]: " (never empty, first class past StackTraceUtils)
          */
        public static String getClassMethodLine()
        {
            return getClassMethodLine(null);
        }

        /**
          * Returns the first "[class#method(line)]: " of the first class not equal to "StackTraceUtils" and aclass. <br />
          * Allows to get past a certain class.
          * @param aclass class to get pass in the stack trace. If null, only try to get past StackTraceUtils. 
          * @return "[class#method(line)]: " (never empty, because if aclass is not found, returns first class past StackTraceUtils)
          */
        public static String getClassMethodLine(final Class aclass)
        {
            final StackTraceElement st = getCallingStackTraceElement(aclass);
            final String amsg = "[" + st.getClassName() + "#" + st.getMethodName() + "(" + st.getLineNumber()
            +")] <" + Thread.currentThread().getName() + ">: ";
            return amsg;
        }

     /**
       * Returns the first stack trace element of the first class not equal to "StackTraceUtils" or "LogUtils" and aClass. <br />
       * Stored in array of the callstack. <br />
       * Allows to get past a certain class.
       * @param aclass class to get pass in the stack trace. If null, only try to get past StackTraceUtils. 
       * @return stackTraceElement (never null, because if aClass is not found, returns first class past StackTraceUtils)
       * @throws AssertionFailedException if resulting statckTrace is null (RuntimeException)
       */
      public static StackTraceElement getCallingStackTraceElement(final Class aclass)
      {
        final Throwable           t         = new Throwable();
        final StackTraceElement[] ste       = t.getStackTrace();
        int index = 1;
        final int limit = ste.length;
        StackTraceElement   st        = ste[index];
        String              className = st.getClassName();
        boolean aclassfound = false;
        if(aclass == null)
        {
            aclassfound = true;
        }
        StackTraceElement   resst = null;
        while(index < limit)
        {
            if(shouldExamine(className, aclass) == true)
            {
                if(resst == null)
                {
                    resst = st;
                }
                if(aclassfound == true)
                {
                    final StackTraceElement ast = onClassfound(aclass, className, st);
                    if(ast != null)
                    {
                        resst = ast;
                        break;
                    }
                }
                else
                {
                    if(aclass != null && aclass.getName().equals(className) == true)
                    {
                        aclassfound = true;
                    }
                }
            }
            index = index + 1;
            st        = ste[index];
            className = st.getClassName();
        }
        if(resst == null) 
        {
            //Assert.isNotNull(resst, "stack trace should null"); //NO OTHERWISE circular dependencies 
            throw new AssertionFailedException(StackTraceUtils.getClassMethodLine() + " null argument:" + "stack trace should null"); //$NON-NLS-1$
        }
        return resst;
      }

      static private boolean shouldExamine(String className, Class aclass)
      {
          final boolean res = StackTraceUtils.class.getName().equals(className) == false && (className.endsWith("LogUtils"
            ) == false || (aclass !=null && aclass.getName().endsWith("LogUtils")));
          return res;
      }

      static private StackTraceElement onClassfound(Class aclass, String className, StackTraceElement st)
      {
          StackTraceElement   resst = null;
          if(aclass != null && aclass.getName().equals(className) == false)
          {
              resst = st;
          }
          if(aclass == null)
          {
              resst = st;
          }
          return resst;
      }

我需要一个能够与Java 1.4兼容的东西,这个答案非常有帮助!谢谢! - RGO

6
我以前做过这个。您只需创建一个新的异常并在其中捕获堆栈跟踪,而无需抛出它,然后检查堆栈跟踪。正如其他答案所说,这非常耗费资源--不要在紧密循环中执行此操作。
我以前为应用程序上的日志实用程序完成过此操作,在该应用程序中性能并不太重要(实际上很少有性能真正重要的情况,只要将结果快速显示给诸如按钮单击之类的操作即可)。
在可以获取堆栈跟踪之前,异常只有.printStackTrace(),因此我必须将System.out重定向到我自己创建的流,然后(new Exception()).printStackTrace();将System.out重定向回来并解析流。有趣的东西。

酷!你不用抛出它? - krosenvold
不是的,至少我记得是这样,我已经有几年没做过了,但我相信新建一个异常就是创建一个对象,而将异常抛出并不会对它做任何事情,只是将其传递到catch()子句。 - Bill K
好的,我想抛出它以模拟实际异常。 - Sathish
不,自从Java 5以来,Thread上有一个方法可以将当前堆栈作为StackTraceElements数组获取;它仍然不便宜,但比旧的异常解析解决方案更便宜。 - Lawrence Dol
@Software Monkey 虽然我相信这更合适,但是你为什么说它更便宜呢?我会假设使用相同的机制,如果不是,那么为什么要让一个做同样的事情的东西变慢呢? - Bill K
@Bill:因为你不需要先生成然后解析文本输出来获取信息——它已经存在于生成文本输出的对象数组中。 - Lawrence Dol

1
private void parseExceptionContents(
      final Exception exception,
      final OutputStream out)
   {
      final StackTraceElement[] stackTrace = exception.getStackTrace();
      int index = 0;
      for (StackTraceElement element : stackTrace)
      {
         final String exceptionMsg =
              "Exception thrown from " + element.getMethodName()
            + " in class " + element.getClassName() + " [on line number "
            + element.getLineNumber() + " of file " + element.getFileName() + "]";
         try
         {
            out.write((headerLine + newLine).getBytes());
            out.write((headerTitlePortion + index++ + newLine).getBytes() );
            out.write((headerLine + newLine).getBytes());
            out.write((exceptionMsg + newLine + newLine).getBytes());
            out.write(
               ("Exception.toString: " + element.toString() + newLine).getBytes());
         }
         catch (IOException ioEx)
         {
            System.err.println(
                 "IOException encountered while trying to write "
               + "StackTraceElement data to provided OutputStream.\n"
               + ioEx.getMessage() );
         }
      }
   }

0

简短回答:ReflectionUtils.getCallingClass(0)

详细回答(代码,Groovy)

package my

import org.codehaus.groovy.reflection.ReflectionUtils
import java.lang.reflect.Field
import java.lang.reflect.Method

trait Reflector {

    static String[] fieldNames() {
        List<String> names = []
        Arrays.asList(naturalFields()).forEach { Field fl -> names.add(fl.name) }
        return names.toArray() as String[]
    }

    static Field[] naturalFields() {
        return finalClass().getDeclaredFields().findAll { Field fl -> !fl.synthetic }.collect()
    }

    static Method[] naturalMethods() {
        return finalClass().getDeclaredMethods().findAll { Method md -> !md.synthetic }.collect()
    }

    static Class finalClass() {
        return ReflectionUtils.getCallingClass(0)
    }

}

class Demo implements Reflector {

    int archived = 0
    int demo = 100

    static void playToo() {
        println finalClass()
    }

}

println Demo.finalClass() // class my.Demo
println Demo.naturalFields() // [private int my.Demo.archived, private int my.Demo.demo]
println Demo.fieldNames() // [archived, demo]

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接