【问题标题】:How to get source file-name/line-number from a java.lang.Class object如何从 java.lang.Class 对象获取源文件名/行号
【发布时间】:2011-09-20 10:04:43
【问题描述】:

给定java.lang.Class 对象,是否有可能获取源文件名和声明类的行号?

数据应该在.class 文件的调试信息中可用。我所知道的JDK返回此类调试信息的唯一地方是java.lang.StackTraceElement,但我不确定是否可以强制Java为任意类创建java.lang.StackTraceElement实例,因为我们没有执行方法在课堂上。

我的确切用例是一个匿名内部类,它有一个编译器生成的名称。我想知道类声明的文件名和行号。

我不喜欢使用字节码操作框架,但如果需要,我可以回退到它。

【问题讨论】:

  • 为什么?通过确定这些信息,您希望为最终用户提供什么功能?
  • @AndrewThompson 在调试应用程序时,您会看到一个具有多个实例的泛型类的对象。但是所有这些实例都引用了Listener 实例,其中Listener 是一个接口或抽象类。您可以通过 Listener 引用的具体类来区分它们:如果它是 XxxListener,您阅读 XxxListener.java 并了解它会做什么,但如果 ListenerSomeOuterClass$12的实例?
  • @saintali - 您刚刚提出了为什么应该将所有侦听器定义为命名类的论据。它将添加 一个 额外的代码行,但会简化调试,使您创建 GUI 对象的方法更具可读性,并使您的模块更易于整体维护。
  • @parsifal - 你的论点实际上意味着完全禁止匿名类,因为它们无论如何都不会获得有意义的名称,而且没有名称的调试很困难。我不同意那个论点。匿名内部类是一种有用的语言特性,当 lambda 语法糖被添加到 Java 中时,它们将变得更加普遍。所以我相信我们必须找到一种方法来启用调试,而不是相反。
  • @saintali - 我的论点并不意味着禁止匿名内部类,只是限制它们的使用。对于短期或有限范围的操作,我没有问题。长期存在或范围广泛的事物应该有自己的名字。

标签: java reflection debug-symbols


【解决方案1】:

这个问题的答案归结为您对实现侦听器的代码有多少控制权。你是对的,没有方法就不可能创建堆栈跟踪。

一般技术是在构造函数中创建一个 Exception(),但不要抛出它。这包含堆栈跟踪信息,您可以根据需要使用这些信息。这将为您提供构造函数的行号,而不是类的行号。请注意,这种方法也不是特别高效,因为创建堆栈跟踪的成本很高。

您需要:

  1. 强制在构造函数中执行一些代码(如果您的 Listener 是您控制的抽象类,则相对容易)
  2. 以某种方式检测代码(这里的治疗方法似乎比疾病更糟糕)。
  3. 对类的命名方式做出一些假设。
  4. 读取 jar(与 javac -p 执行相同操作)

对于 1),您只需将异常创建放在抽象类中,然后子类调用构造函数:

class Top {
    Top() {
        new Exception().printStackTrace(System.out);
    }
}

class Bottom extends Top {
    public static void main(String[] args) {
        new Bottom();
    }
}

这会产生类似:

java.lang.Exception
    at uk.co.farwell.stackoverflow.Top.<init>(Top.java:4)
    at uk.co.farwell.stackoverflow.Bottom.<init>(Bottom.java: 11)
    at uk.co.farwell.stackoverflow.Bottom.main(Bottom.java: 18)

一般来说,遵循一些命名规则:如果你有一个名为Actor的外部类和一​​个名为Consumer的内部类,那么编译后的类将称为Actor$Consumer。匿名内部类是按照它们在文件中出现的顺序命名的,所以 Actor$1 将在 Actor$2 之前出现在文件中。我不认为这实际上是在任何地方指定的,所以这可能只是一个约定,如果您正在使用多个 jvm 等进行复杂的操作,则不应依赖它。

正如 jmg 所指出的,您可以在同一个文件中定义多个顶级类。如果你有一个公共类 Foo,它必须在 Foo.java 中定义,但一个非公共类可以包含在另一个文件中。上面的方法可以解决这个问题。

解释:

如果你反汇编java(javap -c -verbose),你会看到调试信息中有行号,但它们只适用于方法。使用以下内部类:

static class Consumer implements Runnable {
    public void run() {
        // stuff
    }
}

javap 输出包含:

uk.co.farwell.stackoverflow.Actors$Consumer();
  Code:
   Stack=1, Locals=1, Args_size=1
   0:   aload_0
   1:   invokespecial   #10; //Method java/lang/Object."<init>":()V
   4:   return
  LineNumberTable: 
   line 20: 0

  LocalVariableTable: 
   Start  Length  Slot  Name   Signature
   0      5      0    this       Luk/co/farwell/stackoverflow/Actors$Consumer;

LineNumberTable 包含适用于方法的行号列表。所以我的 Consumer 构造函数从第 20 行开始。但这是构造函数的第一行,而不是类的第一行。它只是同一行,因为我使用的是默认构造函数。如果我添加一个构造函数,那么行号将会改变。编译器不存储声明类的行。因此,如果不解析 java 本身,您将无法找到声明类的位置。你根本没有可用的信息。

但是,如果您使用的是匿名内部类,例如:

Runnable run = new Runnable() {
    public void run() {
    }
};

那么构造函数和类的行号会匹配[*],所以这会给你一个行号。

[*] 除非“new”和“Runnable()”在不同的行。

【讨论】:

  • 我发现一个匿名内部类恰好有一个getDeclaredConstructors(它怎么会有更多或更少?)并且该构造函数的行号是匿名类本身的行号。你能把这个加入你的答案吗?
  • 不需要创建异常,使用Thread.currentThread().getStackTrace()
【解决方案2】:

你可以找到uut当前的代码行:

Throwable t = new Throwable();
System.out.println(t.getStackTrace()[0].getLineNumber());

但看起来StackTraceElements 是由Throwable 内部的原生JDK 方法创建的。

public synchronized native Throwable fillInStackTrace();

如果您使用字节码操作框架,向创建 throwable 的类添加方法,您将无法获得正确的类声明代码行。

【讨论】:

  • 我认为在类中获取 some 方法的行号仍然很有帮助,但我相信以这种方式添加的方法运行时不会包含调试信息,所以它将指向文件的开头。如果文件包含很多非公共类或内部类,这将没有什么价值。
【解决方案3】:

您可以通过调用getStackTrace() 从任何线程获取堆栈跟踪。因此,对于当前线程,您必须调用 Thread.currentThread().getStackTrace()

【讨论】:

    【解决方案4】:

    出于您的目的,仅为其堆栈跟踪生成异常是正确的答案。

    但在这不起作用的情况下,您也可以使用Apache BCEL 来分析Java 字节码。如果这听起来像是矫枉过正,那么您可能是对的。

    public boolean isScala(Class jvmClass) {
       JavaClass bpelClass = Repository.lookupClass(jvmClass.getName());
       return (bpelClass.getFileName().endsWith(".scala");
    }
    

    (警告:我尚未测试此代码。)

    另一种选择是构建自定义 doclet 以在编译时收集适当的元数据。我过去曾将它用于需要在运行时了解特定超类的所有子类的应用程序。将它们添加到工厂方法太笨拙了,这也让我可以直接链接到 javadoc。

    现在我正在使用 Scala 编写新代码,我正在考虑使用类似上述的技术,并通过搜索构建目录来生成类列表。

    【讨论】:

      【解决方案5】:

      我就是这样做的:

      import java.io.PrintWriter;
      import java.io.StringWriter;
      
      /**
       * This class is used to determine where an object is instantiated from by extending it. 
       */
      public abstract class StackTracer {
      
          protected StackTracer() {
              System.out.println( shortenedStackTrace( new Exception(), 6 ) );
          }
      
          public static String shortenedStackTrace(Exception e, int maxLines) {
              StringWriter writer = new StringWriter();
              e.printStackTrace( new PrintWriter(writer) );
              String[] lines = writer.toString().split("\n");
              StringBuilder sb = new StringBuilder();
              for (int i = 0; i < Math.min(lines.length, maxLines); i++) {
                  sb.append(lines[i]).append("\n");
              }
              return sb.toString();
          }
      
      }
      

      编辑:回想起来,我现在可能会使用 AOP 来执行此操作。事实上,对this project 进行一些小改动,我或许可以做到。 Here is a stack question 有提示。

      【讨论】:

        【解决方案6】:

        这个答案不包括行号,但它满足了我的需要,应该适用于你拥有的任何源文件。

        这是针对具有多个项目的 IDE 根据类文件查找源文件位置的答案。它利用类加载器和您的 java 项目约定来解决这个问题。

        您需要更新源文件夹 (src) 和输出文件夹 (bin) 以符合您的约定。您必须提供自己的

        实现
        String searchReplace(old, new, inside)
        

        这里是代码

        public static String sourceFileFromClass(Class<?> clazz) {
            String answer = null;
            try {
                if (clazz.getEnclosingClass() != null) {
                    clazz = clazz.getEnclosingClass();
                }
                String simpleName = clazz.getSimpleName();
        
                // the .class file should exist from where it was loaded!
                URL url = clazz.getResource(simpleName + ".class");
                String sourceFile = searchReplace(".class", ".java", url.toString());
                sourceFile = searchReplace("file:/", "", sourceFile);
                sourceFile = searchReplace("/bin/", "/src/", sourceFile);
                if( new java.io.File(sourceFile).exists() ) {
                    answer = sourceFile;
                }
            }
            catch (Exception ex) {
                // ignore
            }
        
            return answer;
        }
        

        【讨论】:

          【解决方案7】:

          是否可以给定一个 java.lang.Class 实例来获取源文件名和声明该类的行号?

          源文件在大多数情况下与类名密切相关。该文件中只有一行声明了该类。这种明确的信息不需要在调试信息中编码。

          数据应该在 .class 文件的调试信息中可用

          为什么?

          【讨论】:

          • 好的,类文件格式中至少包含方法调试信息。只需给我类的 any 声明方法的文件名和行号(如果有)。有可能吗?
          • 在一个 Java 源代码文件中包含多个 顶级 类并不常见,但很有可能。然后,源和文件名之间没有相关性 --- 仅与它的文件路径有关。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2013-09-29
          • 1970-01-01
          • 2016-11-14
          • 1970-01-01
          • 2011-12-14
          • 1970-01-01
          相关资源
          最近更新 更多