I want to add/replace the SourceFile
attribute in a compiled Java .class
file. I have not noticed any obscure command-line options to the Java compiler to override the default value of SourceFile
. Neither have I seen anything in the Java reflection API that would help me. Skim reading chapter 4 of the JVM specification indicates I could spend a few weeks writing a .class
file parser/modifier to do the work. Before I invest the effort to write such a parser/modifier, I want to check if I am missing something. Is there anything in the standard JDK to help with adding/replacing the SourceFile
attribute?
For anyone wondering why I would want to mess with the SourceFile
attribute... I have a command-line tool that preprocesses a "Java enhanced with some syntactic sugar" file into Java syntax. The file-name extension for this type of file is .bi
. Thus, the preprocessor converts Foo.bi
into Foo.java
. In addition, there is line-number correspondence between Foo.bi
and Foo.java
, so if a runtime error occurs on, say, line 42 of Foo.java
, then the bug should really be fixed on line 42 of Foo.bi
(and then run the preprocessor and compile the updated Foo.java
file). To facilitate this, I would like the stack trace of the error to indicate Foo.bi
rather than Foo.java
, and my experiments suggest this can be achieved by ensuring the Foo.class
file has a SourceFile
attribute with a value of Foo.bi
.
There is no such feature in the standard JDK API, but before trying to implement a bytecode processor yourself, consider using a 3rd party library like ASM. With that library, the task could be implemented like:
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;
}
The relevant part is the changeSourceAttr
method. It will change the source file attribute if present or add a new one otherwise.
The example changes the attribute for the java.lang.Object
class using a temporary file and runs javap
to show the result. The example code requires JDK9+, the actual transformation method should work on previous versions too. When the source file does not originate from the JDK library, source and target may be the same file.