如何从java.lang.Class对象获取源文件名/行号

时间:2011-09-20 10:04:44

标签: java reflection debug-symbols

在给定java.lang.Class对象的情况下,是否可以获取源文件名和声明类的行号?

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

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

我不想使用字节码操作框架,但如果必须的话,我可以回到它。

7 个答案:

答案 0 :(得分:4)

这个问题的答案取决于你对实现Listener的代码有多少控制权。你是对的,不能在没有方法的情况下创建堆栈跟踪。

一般技术是在构造函数中创建Exception(),但不要抛出它。它包含stacktrace信息,您可以按照自己的方式使用它。这将为您提供构造函数的行号,但不包含类的行号。请注意,此方法也不是特别有效,因为创建堆栈跟踪非常昂贵。

您需要:

  1. 强制在构造函数中执行一些代码(如果你的Listener是你控制的抽象类,则相对容易)
  2. 以某种方式对代码进行检测(治疗方法似乎比这里的疾病更糟)。
  3. 对类的命名方式做一些假设。
  4. 阅读jar(与javac -p相同)
  5. 对于1),您只需将Exception创建放在抽象类中,构造函数就会被子类调用:

    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之前的文件中。我不认为这实际上是在任何地方指定的,所以这可能只是一个约定,如果你正在做任何复杂的多个jvms等,不应该依赖它。

    正如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()”在不同的行上。

答案 1 :(得分:3)

您可以找到当前的代码行:

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

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

public synchronized native Throwable fillInStackTrace();

事件如果你使用字节码操作框架,将一个方法添加到一个创建throwable的类中,你将无法获得正确的类声明代码。

答案 2 :(得分:1)

您可以通过调用getStackTrace()从任何线程获取堆栈跟踪。因此,您需要拨打Thread.currentThread().getStackTrace()

答案 3 :(得分:1)

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

但是在不起作用的情况下,您还可以使用Apache BCEL来分析Java字节代码。如果这听起来像是笨拙的过度杀戮,你可能是对的。

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

(警告:我没有测试过这段代码。)

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

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

答案 4 :(得分:1)

我就这样做了:

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提示。

答案 5 :(得分:0)

  

是否可以通过java.lang.Class实例获取源文件名和声明类的行号?

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

  

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

为什么?

答案 6 :(得分:0)

此答案不包括行号,但它满足了我的需求,并且适用于您拥有的任何源文件。

这是根据具有多个项目的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;
}