java - 使用 ASM 将 RunTimeVisibleAnnotations 添加到 Java 类文件
问题描述
我正在开发一个项目,该项目需要我直接在现有类文件中向局部变量添加注释,以便它重新创建下面 Java 代码的效果。
@Target({ElementType.LOCAL_VARIABLE, ElementType.TYPE, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {}
public class MyClass {
public static void main(String[] args) {
int x = 512;
@MyAnnotation int y = 5;
...
}
}
类文件输出的注释部分javap -p -v
如下所示。
...
RuntimeVisibleTypeAnnotations:
0: #15(): LOCAL_VARIABLE, {start_pc=6, length=26, index=2}
MyAnnotation
为此,我一直在研究 ASM。现在我对 ASM 没有任何经验,但是看到一些例子,我想我对如何进行有了一些想法。这是我的尝试。
public class ASMTransformer {
public static void main(String[] args) throws Exception {
FileInputStream fis = new FileInputStream(args[0]);
ClassReader cr = new ClassReader(fis);
ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES | ClassWriter.COMPUTE_MAXS);
cr.accept(new ASMClass(cw), ClassReader.EXPAND_FRAMES);
FileOutputStream fos = new FileOutputStream(args[0]);
fos.write(cw.toByteArray());
fos.close();
}
public static class ASMClass extends ClassVisitor {
public ASMClass(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
if (name == "main")
return new ASMMethod(super.visitMethod(access, name, desc, signature, exceptions));
else
return super.visitMethod(access, name, desc, signature, exceptions);
}
}
public static class ASMMethod extends MethodVisitor {
public ASMMethod(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
public void visitEnd() {
super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible);
super.visitEnd();
}
}
}
我visitLocalVariableAnnotation()
通过从带注释的类文件中打印出一些参数的值来计算它们的值。但是变量的start
Label[]
, theend
Label[]
和 theindex
是我无法弄清楚的。
有人可以验证我是否朝着正确的方向前进并帮助我获得这些论点的价值吗?
解决方案
这些数组允许您指定多个变量和范围以应用注释。这允许更短的类文件,特别是对于具有多个(相同)值的注释。
最简单的方法是传递长度为 1 的数组,指定x
变量及其范围。您可以从局部变量表中获取所需的信息,假设该类已经编译并包含调试信息。
public static class ASMMethod extends MethodVisitor {
public ASMMethod(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitLocalVariable(String name, String descriptor, String signature,
Label start, Label end, int index) {
super.visitLocalVariable(name, descriptor, signature, start, end, index);
if(name.equals("x")) {
super.visitLocalVariableAnnotation(TypeReference.LOCAL_VARIABLE << 24, null,
new Label[] { start }, new Label[] { end }, new int[]{ index },
"LMyAnnotation;", true)
.visitEnd();
}
}
}
这会产生这个 javap 输出,表明注释信息已经存储了一次 forx
和一次 for y
。
RuntimeVisibleTypeAnnotations:
0: #41(): LOCAL_VARIABLE, {start_pc=4, length=28, index=1}
MyAnnotation
1: #41(): LOCAL_VARIABLE, {start_pc=6, length=26, index=2}
您可以改为实现“克隆y
的注释”逻辑,这也减少了类文件的大小:
public static class ASMMethod extends MethodVisitor {
private int xIndex, yIndex;
private Label xStart, xEnd, yStart, yEnd;
public ASMMethod(MethodVisitor mv) {
super(Opcodes.ASM9, mv);
}
@Override
public void visitLocalVariable(String name, String descriptor, String signature,
Label start, Label end, int index) {
super.visitLocalVariable(name, descriptor, signature, start, end, index);
if(name.equals("x")) {
xIndex = index;
xStart = start;
xEnd = end;
}
else if(name.equals("y")) {
yIndex = index;
yStart = start;
yEnd = end;
}
}
@Override
public AnnotationVisitor visitLocalVariableAnnotation(int typeRef,
TypePath typePath, Label[] start, Label[] end, int[] index,
String descriptor, boolean visible) {
if(Arrays.stream(index).anyMatch(ix -> ix == yIndex)) {
int num = start.length, newSize = num + 1;
index = Arrays.copyOf(index, newSize);
index[num] = xIndex;
start = Arrays.copyOf(start, newSize);
start[num] = xStart;
end = Arrays.copyOf(end, newSize);
end[num] = xEnd;
}
return super.visitLocalVariableAnnotation(
typeRef, typePath, start, end, index, descriptor, visible);
}
}
y
这会将所有的注释复制到x
,包括它们的值。使用您的示例类,javap
现在指示注释信息是为x
和共享的y
。
RuntimeVisibleTypeAnnotations:
0: #43(): LOCAL_VARIABLE, {start_pc=6, length=26, index=2; start_pc=4, length=28, index=1}
MyAnnotation
关于您的代码的一些附加说明:
不得将字符串与==
. 替换为name == "main"
,name.equals("main")
使其工作。但您也可以减少相同super.visitMethod
调用的代码重复。
根据您正在运行的系统,不关闭FileInputStream
可能会使您覆盖同一文件的尝试失败。但一般建议尽早关闭资源。
这COMPUTE_FRAMES
意味着COMPUTE_MAXS
,所以你不需要将两者结合起来。此外,COMPUTE_FRAMES
表示“从头开始计算所有帧”,因此它不使用原始代码的帧。因此,除非您正在执行其他一些处理,例如 using ,否则您在 usingAnalyzer
时不需要原始帧COMPUTE_FRAMES
。所以传递SKIP_FRAMES
给那个ClassReader
更合适。相反,该选项EXPAND_FRAMES
将对原始帧执行准备工作,以进行此处从未发生过的处理,这会浪费 CPU 周期。
但是当你要做的只是注入注释时,换句话说,你没有更改任何可执行代码,根本没有理由干涉堆栈映射框架。插桩代码可以只保留原始帧,这是最简单和最有效的处理。
结合所有这些点产生
public class ASMTransformer {
public static void main(String[] args) throws IOException {
String clazz = args[0];
byte[] code;
try(InputStream fis = new FileInputStream(clazz)) {
ClassReader cr = new ClassReader(fis);
ClassWriter cw = new ClassWriter(cr, 0);
cr.accept(new ASMClass(cw), 0);
code = cw.toByteArray();
}
Files.write(Paths.get(clazz), code);
}
public static class ASMClass extends ClassVisitor {
public ASMClass(ClassVisitor cv) {
super(Opcodes.ASM9, cv);
}
@Override
public MethodVisitor visitMethod(
int access, String name, String desc, String sig, String[] exceptions) {
MethodVisitor mv = super.visitMethod(access, name, desc, sig, exceptions);
if(name.equals("main")) mv = new ASMMethod(mv);
return mv;
}
}
public static class ASMMethod extends MethodVisitor { // as above
…
}
推荐阅读
- image-processing - 为什么图像压缩不使用重叠数据?
- asynchronous - await vs then - 访问返回值
- github - 如何使用我的 Github webhook 配置运行更少的 CircleCI 作业?
- python - 为什么在 pandas.Series 上应用类型函数在应用于其存储值时返回一个不同的类?
- php - Wordpress meta_key 结构 - 读写
- php - 即使策略返回错误,自动化也会通过
- jquery - 画廊 Jquery 同位素与随机和重叠的图片
- amazon-web-services - 使用 golang 生成 Cognito Web 令牌
- c - 如何正确清除输入缓冲区(第一个项目)#rewritten 代码
- ubuntu - 无法打开模块文件“hdf5.mod”以在 (1) 处读取:没有这样的文件或目录