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



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

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

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

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

这个问题的答案取决于您对实现侦听器的代码有多大的控制。你是对的,如果没有在方法中创建堆栈跟踪是不可能的。

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

你需要:

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

对于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之前出现在文件中。我不认为这实际上在任何地方都有指定,所以这可能只是一种约定,如果你使用多个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"one_answers"Runnable()"在不同的行。

您可以找到当前行代码:

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

但是看起来StackTraceElements是由本地JDK方法在Throwable中创建的。

public synchronized native Throwable fillInStackTrace();
如果你使用字节码操作框架,向类中添加一个方法来创建throwable,你将无法获得适当的类声明代码行。

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

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

但是在不工作的情况下,您也可以使用Apache BCEL来分析Java字节码。如果这听起来像是过度的高压手段,你可能是对的。

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

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

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

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

这是我做到了:

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来做这件事。事实上,只要对这个项目做一些小改动,我就能做到。这是一个带有提示的堆栈问题。

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

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

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

中可用。

为什么?

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

对于有多个项目的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;
}

相关内容

  • 没有找到相关文章