关于java:注释类型可以定义静态方法吗?

Can an annotation type define static methods?

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

1
2
3
4
5
6
7
8
9
10
11
@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接口类型上引入静态方法时(参见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。通过在相关接口中定位实用程序方法,它提高了它们的可发现性,并从API域中删除了可能不必要的助手类类型。

由于当前用于注释类型的jvm字节码表示与普通接口密切相关,我想知道注释是否也支持静态方法。当我将助手移动到注释类型中时,例如:

1
2
3
4
5
6
7
@Target(TYPE)
@Retention(RUNTIME)
public @interface MyAnnotation {
   // ... annotation elements ...

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

…javac抱怨了以下编译时错误,这让我有点惊讶:

openjdk runtime environment 18.3(build 10+46)

  • modifier static not allowed here
  • elements in annotation type declarations cannot declare formal parameters
  • interface abstract methods cannot have body

显然,Java语言目前不允许这样做。这可能是因为有很好的设计理由不允许这样做,或者正如前面对静态接口方法所假定的那样,"没有令人信服的理由这样做;一致性不足以改变现状"。

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

JVM是一种强大的技术,在许多方面比Java语言所允许的更灵活。同时,Java语言也在不断发展,今天的答案可能会在明天过时。理解到这种力量必须谨慎使用…

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


在JVM内实现这一技术并与标准Java代码互操作在技术上是可行的,但它具有重要的注意事项:

  • JAVA兼容的源代码,按JLS,不能在注释类型中定义静态方法。
  • Java源代码似乎能够使用这些方法,如果它们存在,包括在编译时和在运行时通过反射。
  • 主题注释可能需要放在一个单独的编译单元中,以便在处理代码时,它的二进制类可供IDE和javac使用。
  • 这已经在OpenJDK 10热点上得到了验证,但有可能观察到的行为取决于内部细节,这些细节可能在以后的版本中有所更改。
  • 在决定采用这种方法之前,请仔细考虑对长期维护和兼容性的影响。
  • 一个概念证明是成功的,它使用了一种直接操作JVM字节码的机制。

    这个机制很简单。使用另一种语言或字节码操作工具(即ASM),它将发出JVM EDCOX1×0×文件,两者(1)匹配合法Java(语言)注释的功能和外观,(2)还包含具有EDCOX1×1的访问修改集的所需方法实现。该类文件可以单独编译并打包成jar或直接放置在类路径上,此时它可以被您的其他正常Java代码使用。

    下面的步骤将创建对应于以下不太合法的Java注释类型的工作字节码,该类型定义了POC中简单的EDCOX1×2静态函数;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @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()参数的注释类设置为不带默认值的字符串:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    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)注释注释:

    1
    2
    3
    4
    5
    6
    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方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    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文件,或使用自定义类加载器(未显示)直接将其加载到内存中:

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

    笔记:

  • 这需要生成字节码版本52(Java 8)或更高版本,并且只在支持该版本的JVMs下运行。
  • 注释的超级类型是java.lang.Object,它们实现了java.lang.annotation.Annotation接口。
  • methodnode构造函数的两个null参数用于泛型和声明的异常,在本例中都不使用。
  • openjdk 10的hotspot要求在静态方法上将MethodNode.annotationDefault设置为非空值(适当类型),即使在将注释应用到另一个元素时,设置/重写strlen永远不是一个选项。这是一个灰色地带,因为这种方法是"合法的"。hs字节码验证器似乎忽略了acc_静态标志,并假定所有定义的方法都是普通的注释元素。