我开发了一个框架和相应的 API,其中包括运行时可见的注释。 API 还提供了一些帮助程序方法,供客户端在其类具有该注释的对象上使用。可以理解的是,帮助器与注释紧密耦合,但重要的是它们的内部结构与客户端封装在一起。目前,辅助方法是通过注释类型中的静态内部类提供的...
@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 ... */
}
}
}
...但是助手可以很容易地存在于其他一些顶级实用程序类中。无论哪种方式都提供了客户端代码所需的封装量,但是两者都会产生额外的成本来维护完全独立的类型,防止它们实例化,因为所有有用的方法都是静态的,等等。
当 Java 8 在 Java 接口(interface)类型上引入静态方法时(请参阅 JLS 9.4 ),该功能被吹捧为提供以下能力:
... organize helper methods in your libraries; you can keep static methods specific to an interface in the same interface rather than in a separate class.
— from Java Tutorials Interface Default Methods
这已在 JDK 库中使用,以提供诸如 List.of(...)
、Set.of(...)
等实现。 ,而以前此类方法被归入单独的实用程序类,例如 java.util.Collections。通过在其相关接口(interface)中定位实用程序方法,它 improves their discoverability并从 API 域中删除了可能不必要的帮助器类类型。
自从我使用当前的 JVM bytecode representation因为注释类型与普通接口(interface)非常密切相关,我想知道注释是否也支持静态方法。当我将助手移至注释类型时,例如:
@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
// ... annotation elements ...
public static Foo helper(Object mightHaveMyAnnotation) { /* ... */ }
}
...令我有点惊讶的是 javac 提示以下编译时错误:
OpenJDK 运行时环境 18.3(内部版本 10+46)
- modifier static not allowed here
- elements in annotation type declarations cannot declare formal parameters
- interface abstract methods cannot have body
显然,Java 语言目前不允许这样做。可能有充分的设计理由不允许它,或者,如 previously presumed对于静态接口(interface)方法,“没有令人信服的理由这样做;一致性不足以改变现状”。
这个问题的目标不是问“为什么它不起作用?”或“语言应该支持它吗?”,以避免基于意见的答案。
JVM 是一项强大的技术,在许多方面比 Java 语言所允许的更灵活。与此同时,Java 语言不断发展,今天的答案明天可能就过时了。了解必须谨慎使用这种力量......
技术上是否可以将静态行为直接封装在注释类型中,以及如何封装?
最佳答案
在 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
文件放在类路径上,或使用自定义类加载器(未显示)直接将其加载到内存中:
ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] bytecode = cw.toByteArray();
注释:
- 这需要生成字节码版本 52 (Java 8) 或更高版本,并且只能在支持该版本的 JVM 下运行。
- 注释有
java.lang.Object
作为他们的 super 类型,他们实现java.lang.annotation.Annotation
接口(interface)。 - 两个
null
MethodNode 构造函数的参数用于泛型和声明的异常,本示例中均未使用。 - OpenJDK 10 的 HotSpot 必需 设置
MethodNode.annotationDefault
静态方法上的非空值(适当类型),即使设置/覆盖strlen
将注释应用于另一个元素时,永远不会成为一个选项。这是这种方法“合法”的灰色地带。 HS 字节码 validator 似乎忽略了 ACC_STATIcflags并假设所有定义的方法都是普通注释元素。
关于java - 注解类型可以定义静态方法吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/50809531/