我已经开发了一个框架和相应的API,其中包括一个运行时可见的注释。API还提供了一些辅助方法,供客户端在类具有该注释的对象上使用。可以理解的是,助手与注释紧密耦合,但重要的是,从客户端封装它们的内部。helper方法当前是通过注释类型中的静态内部类提供的。。。
@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
// ... annotation elements, e.g. `int xyz();` ...
public static final class Introspection {
public static Foo helper(Object mightHaveMyAnnotation) {
/* ... uses MyAnnotation.xyz() if annotation is present ... */
}
}
}
但是helper可以很容易地存在于其他一些顶级实用程序类中。无论哪种方式都从客户端代码中提供了必要的封装量,但都会产生额外的成本来维护一个完全独立的类型,阻止它们实例化,因为所有有用的方法都是静态的,等等
当Java8在Java接口类型上引入静态方法时(见JLS 9.4),该功能被吹捧为提供了…的能力
。。。在库中组织辅助方法;可以将特定于接口的静态方法保留在同一接口中,而不是单独的类中
—来自Java教程接口默认方法
JDK库中已使用此方法来提供List.of(...)
、Set.of(...)
等实现,而以前此类方法被降级到单独的实用程序类(如java.util.Collections
)中。通过在实用程序方法的相关接口中定位实用程序方法,它提高了它们的可发现性,并从API域中删除了可能不必要的助手类类型。
由于注释类型的当前JVM字节码表示与普通接口密切相关,我想知道注释是否也支持静态方法。当我将助手移动到注释类型中时,例如:
@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
// ... annotation elements ...
public static Foo helper(Object mightHaveMyAnnotation) { /* ... */ }
}
javac抱怨以下编译时错误,这让我有点惊讶:
OpenJDK运行时环境18.3(构建10+46)
此处不允许使用
- 修饰符static
- 注释类型声明中的元素不能声明形式参数
- 接口抽象方法不能有正文
很明显,Java语言目前不允许这样做。这可能是因为有充分的设计理由不允许它,或者,正如之前对静态接口方法所假设的那样,"没有令人信服的理由这样做;一致性不足以改变现状"。
这个问题的目的是问">为什么不起作用?"或">语言应该支持它吗?",以避免基于意见的回答
JVM是一种强大的技术,在很多方面都比Java语言所允许的更灵活。与此同时,Java语言在不断发展,今天的答案明天可能就过时了。有了这样的理解,必须非常小心地使用这种权力。。。
从技术上讲,是否可以将静态行为直接封装在注释类型中,以及如何封装
在JVM中实现这一点并与标准Java代码进行互操作在技术上是可行的,但需要注意:
根据JLS,兼容Java的源代码不能在注释类型中定义静态方法概念验证是成功的,使用了一种直接操纵JVM字节码的机制
这个机制很简单。使用备用语言或字节码操作工具(即ASM),它将发出JVM*.class
文件,该文件(1)与合法Java(语言)注释的功能和外观匹配,(2)还包含带有static
访问修饰符集的所需方法实现。这个类文件可以单独编译并打包到JAR中,也可以直接放在类路径中,这时它就可以被其他普通Java代码使用了。
以下步骤将创建与以下不完全合法的Java注释类型相对应的工作字节码,该注释类型在POC:中定义了一个简单的strlen
静态函数
@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!
}
}
首先,将带有"normal"value()
参数的注释类设置为没有默认值的String:
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(Java8)或更高版本,并且只能在支持该版本的JVM下运行
- 注释将
java.lang.Object
作为它们的超类型,并且它们实现java.lang.annotation.Annotation
接口 - MethodNode构造函数的两个
null
参数用于泛型和声明的异常,在本例中均未使用 - OpenJDK 10的HotSpot需要在静态方法上将
MethodNode.annotationDefault
设置为非null值(适当类型),即使当将注释应用于另一个元素时,设置/覆盖strlen
永远不是一个选项。这是一个灰色地带,这种做法是"合法的"。HS字节码验证器似乎忽略了ACC_STATIC标志,并假设所有定义的方法都是正常的注释元素