如何从java.lang.Class对象获取源文件名/行号
在给定java.lang.Class
对象的情况下,是否可以获取源文件名和声明类的行号?
数据应该在.class
文件的调试信息中提供。 我知道的唯一一个JDK返回此类调试信息的地方是java.lang.StackTraceElement
但我不确定是否可以强制Java为任意类创建java.lang.StackTraceElement
实例,因为我们是不在类中执行方法。
我的确切用例是一个匿名内部类,它有一个编译器生成的名称。 我想知道类声明的文件名和行号。
我不想使用字节码操作框架,但如果必须的话,我可以回到它。
对此的答案取决于您对实现Listener的代码的控制程度。 你是对的,不能在没有方法的情况下创建堆栈跟踪。
一般的技术是在构造函数中创建一个Exception(),但不要抛出它。 它包含stacktrace信息,您可以按照自己的方式使用它。 这将为您提供构造函数的行号,但不包含类的行号。 请注意,此方法也不是特别高效,因为创建堆栈跟踪非常昂贵。
你需要:
- 强制一些代码在构造函数中执行(如果你的Listener是你控制的抽象类,相对容易)
- 以某种方式检测代码(治疗似乎比这里的疾病更糟)。
- 对类的命名方式做一些假设。
- 阅读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.(Top.java:4) at uk.co.farwell.stackoverflow.Bottom. (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."":()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()”在不同的行上。
你可以找到当前的代码行:
Throwable t = new Throwable(); System.out.println(t.getStackTrace()[0].getLineNumber());
但看起来StackTraceElements
是由Throwable
本机JDK方法创建的。
public synchronized native Throwable fillInStackTrace();
事件如果你使用字节码操作框架,向一个创建throwable的类添加一个方法,你将无法获得类声明的正确代码行。
您可以通过调用getStackTrace()从任何线程获取堆栈跟踪。 因此,当前线程必须调用Thread.currentThread().getStackTrace()
。
出于您的目的,仅为其堆栈跟踪生成exception是正确的答案。
但是在不起作用的情况下,您也可以使用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; }