在 JVM 中完成此操作并与标准 Java 代码互操作在技术上是可行的,但它带有重要的警告:
- 根据 JLS,Java 兼容的源代码不能在注释类型中定义静态方法。
- 如果存在这些方法, Java 源代码似乎能够使用这些方法,包括在编译时和运行时通过反射。
- 主题注释可能需要放在单独的编译单元中,以便在处理代码时它的二进制类可用于 IDE 和javac 。
- 这已在 OpenJDK 10 HotSpot 上得到验证,但观察到的行为可能取决于内部细节,可能会在以后的版本中发生变化。
- 在决定采用这种方法之前,请仔细考虑对长期维护和兼容性的影响。
使用一种直接操纵 JVM 字节码的机制,概念验证是成功的。
机制很简单。使用替代语言或字节码操作工具(即 ASM),它将发出一个 JVM*.class文件,该文件 (1) 与合法Java(语言)注释的功能和外观相匹配,并且 (2) 还包含所需的方法实现使用static访问修饰符集。这个类文件可以单独编译并打包到 JAR 中或直接放在类路径中,此时它可以被其他普通 Java 代码使用。
以下步骤将创建与以下不太合法的Java 注释类型相对应的工作字节码,它strlen在 POC 中为简单起见定义了一个简单的静态函数:
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value();
// not legal in Java, through at least JDK 10:
public static int strlen(java.lang.String str) {
return str.length(); // boring!
}
}
首先,将带有“普通”value()参数的注释类设置为没有默认值的字符串:
import static org.objectweb.asm.Opcodes.*;
import java.util.*;
import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;
/* ... */
final String fqcn = "com.example.MyAnnotation";
final String methodName = "strlen";
final String methodDesc = "(Ljava/lang/String;)I"; // int function(String)
ClassNode cn = new ClassNode(ASM6);
cn.version = V1_8; // Java 8
cn.access = ACC_SYNTHETIC | ACC_PUBLIC | ACC_INTERFACE | ACC_ABSTRACT | ACC_ANNOTATION;
cn.name = fqcn.replace(".", "/");
cn.superName = "java/lang/Object";
cn.interfaces = Arrays.asList("java/lang/annotation/Annotation");
// String value();
cn.methods.add(
new MethodNode(
ASM6, ACC_PUBLIC | ACC_ABSTRACT, "value", "()Ljava.lang.String;", null, null));
如果合适,可以选择用 注释注释@Retention(RUNTIME):
AnnotationNode runtimeRetention = new AnnotationNode(ASM6, "Ljava/lang/annotation/Retention;");
runtimeRetention.values = Arrays.asList(
"value", // parameter name; related value follows immediately next:
new String[] { "Ljava/lang/annotation/RetentionPolicy;", "RUNTIME" } // enum type & value
);
cn.visibleAnnotations = Arrays.asList(runtimeRetention);
接下来,添加所需的static方法:
MethodNode method = new MethodNode(ASM6, 0, methodName, methodDesc, null, null);
method.access = ACC_PUBLIC | ACC_STATIC;
method.annotationDefault = Integer.MIN_VALUE; // see notes
AbstractInsnNode invokeStringLength =
new MethodInsnNode(INVOKEVIRTUAL, "java/lang/String", "length", "()I", false);
method.instructions.add(new IntInsnNode(ALOAD, 0)); // push String method arg
method.instructions.add(invokeStringLength); // invoke .length()
method.instructions.add(new InsnNode(IRETURN)); // return an int value
method.maxLocals = 1;
method.maxStack = 1;
cn.methods.add(method);
最后,将此注释的 JVM 字节码输出到*.class类路径上的文件中,或者使用自定义的 ClassLoader(未显示)将其直接加载到内存中:
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] bytecode = cw.toByteArray();
笔记:
- 这需要生成字节码版本 52 (Java 8) 或更高版本,并且只能在支持该版本的 JVM 下运行。
- 注释
java.lang.Object作为它们的超类型,它们实现了java.lang.annotation.Annotation接口。
- MethodNode 构造函数的两个
null参数用于泛型和声明的异常,本示例中均未使用。
- OpenJDK 10 的 HotSpot要求在静态方法上设置
MethodNode.annotationDefault为非空值(适当类型),即使在将注释应用于另一个元素strlen时设置/覆盖永远不会是一个选项。这是这种“合法”方法的灰色地带。HS 字节码验证器似乎忽略了 ACC_STATIC 标志,并假定所有定义的方法都是正常的注释元素。