如何 add/replace .class 文件中的 SourceFile 属性

How to add/replace the SourceFile attribute in a .class file

我想 add/replace 已编译的 Java .class 文件中的 SourceFile 属性。我没有注意到 Java 编译器有任何模糊的命令行选项来覆盖 SourceFile 的默认值。我也没有在 Java 反射 API 中看到任何对我有帮助的东西。略读 JVM 规范的第 4 章表明我可以花几周时间编写一个 .class 文件 parser/modifier 来完成这项工作。在我投入精力编写这样一个 parser/modifier 之前,我想检查一下我是否遗漏了什么。标准 JDK 中是否有任何内容可以帮助 adding/replacing SourceFile 属性?

对于任何想知道我为什么要弄乱 SourceFile 属性的人...我有一个命令行工具可以将“Java 增强了一些语法糖”文件预处理成 Java语法。此类文件的文件扩展名为 .bi。因此,预处理器将 Foo.bi 转换为 Foo.java。此外,Foo.biFoo.java 之间存在行号对应关系,因此如果 运行time 错误发生在 Foo.java 的第 42 行,则错误应该真正固定在 Foo.bi 的第 42 行(然后 运行 预处理器并编译更新的 Foo.java 文件)。为了促进这一点,我希望错误的堆栈跟踪指示 Foo.bi 而不是 Foo.java,我的实验表明这可以通过确保 Foo.class 文件具有 [=12] =] 属性值为 Foo.bi.

标准 JDK API 中没有这样的功能,但在尝试自己实现字节码处理器之前,请考虑使用像 ASM 这样的第三方库。使用该库,任务可以像这样实现:

public static void main(String[] args) throws IOException {
    Path in = Paths.get(URI.create("jrt:/java.base/java/lang/Object.class"));
    Path out = Files.createTempFile("Object", ".class");

    changeSourceAttr(in, out, "Object.bi");

    runJavaP(out);

    Files.delete(out);
}

private static void changeSourceAttr(Path in, Path out, String newValue)
    throws IOException {

    ClassReader cr = new ClassReader(Files.readAllBytes(in));
    ClassWriter cw = new ClassWriter(cr, 0);
    cr.accept(new ClassVisitor(Opcodes.ASM5, cw) {
        boolean sourceSeen;
        @Override
        public void visitSource(String source, String debug) {
            sourceSeen = true;
            super.visitSource(newValue, debug);
        }

        @Override
        public void visitEnd() {
            if(!sourceSeen) {
                super.visitSource(newValue, null);
            }
            super.visitEnd();
        }
    }, 0);
    byte[] code = cw.toByteArray();
    Files.write(out, code);
}

private static void runJavaP(Path out) {
    ToolProvider.findFirst("javap")
        .ifPresent(tp -> tp.run(System.out, System.err, out.toString()));
}
Compiled from "Object.bi"
public class java.lang.Object {
  public java.lang.Object();
  public final native java.lang.Class<?> getClass();
  public native int hashCode();
  public boolean equals(java.lang.Object);
  protected native java.lang.Object clone() throws java.lang.CloneNotSupportedException;
  public java.lang.String toString();
  public final native void notify();
  public final native void notifyAll();
  public final void wait() throws java.lang.InterruptedException;
  public final native void wait(long) throws java.lang.InterruptedException;
  public final void wait(long, int) throws java.lang.InterruptedException;
  protected void finalize() throws java.lang.Throwable;
}

相关部分是changeSourceAttr方法。它将更改源文件属性(如果存在)或添加新属性。

该示例使用临时文件更改 java.lang.Object class 的属性并运行 javap 以显示结果。示例代码需要 JDK9+,实际转换方法应该也适用于以前的版本。当源文件不是来自 JDK 库时,源文件和目标文件可能是同一个文件。