生成局部变量信息相当容易。在目标方法访问者上发出正确的visitLocalVariable 调用,声明局部变量的名称、类型和范围。这将在类文件中生成LocalVariableTable attribute。
当涉及到源代码级调试时,这些工具将简单地在类中查找SourceFile attribute 以获取要加载和显示的文本文件的名称。您可以通过在目标类访问者 (ClassWriter) 上调用 visitSource(fileName, null) 来生成它。指定文本文件和字节码指令之间的关系可以通过在目标方法访问者上调用visitLineNumber 来声明。对于普通的源代码,你只需要在相关行发生变化时调用它。但是对于字节码表示,它会随着每条指令而改变,这可能会导致类文件相当大,因此您绝对应该将这些调试信息的生成设为可选。
现在,您只需要生成文本文件。您可以将目标 ClassWriter 包装在 TraceClassVisitor 中,然后将其传递给代码生成器,以便在生成代码时生成人类可读的形式。但是我们必须扩展 ASM 提供的Textifier,因为我们需要跟踪缓冲文本的行号,并且还希望抑制为我们的行号信息本身生成输出,这会使源代码变得混乱,增加两行每条指令。
public class LineNumberTextifier extends Textifier {
private final LineNumberTextifier root;
private boolean selfCall;
public LineNumberTextifier() { super(ASM5); root = this; }
private LineNumberTextifier(LineNumberTextifier root) { super(ASM5); this.root = root; }
int currentLineNumber() { return count(super.text)+1; }
private static int count(List<?> text) {
int no = 0;
for(Object o: text)
if(o instanceof List) no+=count((List<?>)o);
else {
String s = (String)o;
for(int ix=s.indexOf('\n'); ix>=0; ix=s.indexOf('\n', ix+1)) no++;
}
return no;
}
void updateLineInfo(MethodVisitor target) {
selfCall = true;
Label l = new Label();
target.visitLabel(l);
target.visitLineNumber(currentLineNumber(), l);
selfCall = false;
}
// do not generate source for our own artifacts
@Override public void visitLabel(Label label) {
if(!root.selfCall) super.visitLabel(label);
}
@Override public void visitLineNumber(int line, Label start) {}
@Override public void visitSource(String file, String debug) {}
@Override protected Textifier createTextifier() {
return new LineNumberTextifier(root);
}
}
然后,你可以像这样一起生成类文件和源文件:
Path targetPath = …
String clName = "TestClass", srcName = clName+".jasm", binName = clName+".class";
Path srcFile = targetPath.resolve(srcName), binFile = targetPath.resolve(binName);
ClassWriter actualCW = new ClassWriter(0);
try(PrintWriter sourceWriter = new PrintWriter(Files.newBufferedWriter(srcFile))) {
LineNumberTextifier lno = new LineNumberTextifier();
TraceClassVisitor classWriter = new TraceClassVisitor(actualCW, lno, sourceWriter);
classWriter.visit(V1_8, ACC_PUBLIC, clName, null, "java/lang/Object", null);
MethodVisitor constructor
= classWriter.visitMethod(ACC_PRIVATE, "<init>", "()V", null, null);
constructor.visitVarInsn(ALOAD, 0);
constructor.visitMethodInsn(
INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
constructor.visitInsn(RETURN);
constructor.visitMaxs(1, 1);
constructor.visitEnd();
MethodVisitor main = classWriter.visitMethod(
ACC_PUBLIC|ACC_STATIC, "main", "([Ljava/lang/String;)V", null, null);
Label start = new Label(), end = new Label();
main.visitLabel(start);
lno.updateLineInfo(main);
main.visitFieldInsn(GETSTATIC, "java/lang/System", "out", "Ljava/io/PrintStream;");
lno.updateLineInfo(main);
main.visitLdcInsn("hello world");
lno.updateLineInfo(main);
main.visitMethodInsn(
INVOKEVIRTUAL, "java/io/PrintStream", "println", "(Ljava/lang/String;)V", false);
lno.updateLineInfo(main);
main.visitInsn(RETURN);
main.visitLabel(end);
main.visitLocalVariable("arg", "[Ljava/lang/String;", null, start, end, 0);
main.visitMaxs(2, 1);
main.visitEnd();
classWriter.visitSource(srcName, null);
classWriter.visitEnd(); // writes the buffered text
}
Files.write(binFile, actualCW.toByteArray());
它生成的“源”文件看起来像
// class version 52.0 (52)
// access flags 0x1
public class TestClass {
// access flags 0x2
private <init>()V
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello world"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
RETURN
L1
LOCALVARIABLE arg [Ljava/lang/String; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 1
}
和javap 报告
Compiled from "TestClass.jasm"
public class TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC
{
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #16 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #18 // String hello world
5: invokevirtual #24 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
LocalVariableTable:
Start Length Slot Name Signature
0 9 0 arg [Ljava/lang/String;
LineNumberTable:
line 17: 0
line 18: 3
line 19: 5
line 20: 8
}
SourceFile: "TestClass.jasm"
示例生成器将两个文件放在同一个目录中,这已经足够jdb 使用它了。当您将文件分别放入类路径时,它也应该与 IDE 调试器一起使用。项目的源路径。
Initializing jdb ...
> stop in TestClass.main
Deferring breakpoint TestClass.main.
It will be set after the class is loaded.
> run TestClass
run TestClass
Set uncaught java.lang.Throwable
Set deferred uncaught java.lang.Throwable
>
VM Started: Set deferred breakpoint TestClass.main
Breakpoint hit: "thread=main", TestClass.main(), line=17 bci=0
17 GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
main[1] locals
Method arguments:
arg = instance of java.lang.String[0] (id=433)
Local variables:
main[1] step
>
Step completed: "thread=main", TestClass.main(), line=18 bci=3
18 LDC "hello world"
main[1] step
>
Step completed: "thread=main", TestClass.main(), line=19 bci=5
19 INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
main[1] step
> hello world
Step completed: "thread=main", TestClass.main(), line=20 bci=8
20 RETURN
main[1] step
>
The application exited
如上所述,当您将这两个文件放入项目的类和源路径时,这也适用于 IDE。我刚刚用 Eclipse 验证了这一点: