注解类型可以定义静态方法吗?

Can an annotation type define static methods?

我开发了一个框架和相应的 API,其中包括一个运行时可见的注释。 API 还提供了一些辅助方法,供客户端在 class 具有该注释的对象上使用。可以理解,帮助程序与注释紧密耦合,但重要的是将它们的内部结构从客户端封装起来。辅助方法目前通过注释类型中的静态内部 class 提供...

@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 ... */
      }
   }
}

...但是助手可以很容易地存在于其他一些顶级实用程序中 class。这两种方式都提供了来自客户端代码的必要封装量,但都需要额外的成本来维护一个完全独立的类型,防止它们被实例化,因为所有有用的方法都是静态的,等等。

当 Java 8 在 Java 接口类型上引入静态方法时(参见 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(...) 等实现,而以前此类方法被归入单独的实用程序 class 例如 java.util.Collections。通过在相关接口中定位实用程序方法,它 improves their discoverability 并从 API 域中删除了可以说是不必要的辅助 class 类型。

因为我当前的 JVM bytecode representation for annotation types 与正常的 interfaces 密切相关,我想知道是否注释也将支持静态方法。当我将 helpers 移动到注释类型中时,例如:

@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
   // ... annotation elements ...

   public static Foo helper(Object mightHaveMyAnnotation) { /* ... */ }
}

... 我有点惊讶 javac 抱怨以下编译时错误:

打开JDK 运行时环境 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 对于静态接口方法,"there [was] no compelling reason to do so; consistency isn't sufficiently compelling to change the status quo".

具体不是这个问题的目的是问“为什么不起作用?”或“语言应该支持它吗?”,以避免基于意见的答案。

JVM 是一项强大的技术,在许多方面比 Java 语言所允许的技术更灵活。同时,Java 语言在不断发展,今天的答案明天可能就过时了。理解必须非常小心地使用这种力量...

技术上是否可以将静态行为直接封装在注释类型中,如何实现?

在 JVM 中完成此操作并与标准 Java 代码互操作在技术上 是可行的,但它有重要的警告:

  1. Java-compatible source code, per the JLS, cannot define static methods in annotation types.
  2. Java 源代码似乎能够 使用 这样的方法 如果它们存在 ,包括在编译时和 运行反射时间。
  3. 主题注释可能需要放在单独的编译单元中,以便其二进制 class 可用于 IDE 和 javac 处理代码时。
  4. 这已在 OpenJDK 10 HotSpot 上得到验证,但观察到的行为可能取决于内部细节,可能会在以后的版本中发生变化。
  5. 在决定采用这种方法之前,请仔细考虑对长期维护和兼容性的影响。

使用直接操作 JVM 字节码的机制,概念验证成功。

机制很简单。使用替代语言或字节码操作工具(即 ASM),它将发出一个 JVM *.class 文件,该文件 (1) 与 合法 [的功能和外观相匹配 Java(语言)注释,并且 (2) 还包含具有 static 访问修饰符集的所需方法实现。这个 class 文件可以单独编译并打包成 JAR 或直接放在 class 路径上,此时它可以被其他正常的 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!
    }
}

首先,将带有 "normal" value() 参数的注释 class 设置为没有默认值的字符串:

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 路径上的 *.class 文件,或使用自定义 ClassLoader(未显示)将其直接加载到内存中:

ClassWriter cw = new ClassWriter(0);
cn.accept(cw);
byte[] bytecode = cw.toByteArray();

备注:

  1. 这需要生成字节码版本 52 (Java 8) 或更高版本,并且只会在支持该版本的 JVM 下 运行。
  2. 注释将 java.lang.Object 作为它们的超类型,并且它们 实现 java.lang.annotation.Annotation 接口。
  3. MethodNode 构造函数的两个 null 参数用于泛型和声明的异常,本例中均未使用。
  4. OpenJDK 10 的 HotSpot 需要 在静态方法上将 MethodNode.annotationDefault 设置为非空值(适当类型),即使 setting/overriding [当 将注释应用 到另一个元素时,=17=] 永远不会是一个选项。这是此方法的灰色区域 "legal"。 HS 字节码验证程序似乎忽略了 ACC_STATIC 标志并假定 所有定义的方法 都是正常的注释元素。