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

Java 注释类型可以定义静态方法吗?,java,annotations,jvm,static-methods,jls,Java,Annotations,Jvm,Static Methods,Jls,我已经开发了一个框架和相应的API,其中包括一个运行时可见的注释。API还提供了一些帮助器方法,供客户端在其类具有该注释的对象上使用。可以理解,helpers与注释紧密耦合,但是从客户端封装它们的内部是很重要的。助手方法当前通过注释类型中的静态内部类提供 @Target(TYPE) @Retention(RUNTIME) public @interface MyAnnotation { // ... annotation elements, e.g. `int xyz();` ...

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

@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 ... */
      }
   }
}
。。。但是助手可以很容易地存在于其他一些顶级实用程序类中。这两种方法都从客户机代码中提供了必要的封装量,但都会产生额外的成本来维护一个完全独立的类型,防止它们实例化,因为所有有用的方法都是静态的,等等

当Java8在Java接口类型上引入静态方法时(请参阅),该特性被吹捧为提供了

。。。在库中组织助手方法;您可以将特定于接口的静态方法保留在同一接口中,而不是单独的类中。

-来自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)

  • 此处不允许使用静态修饰符
  • 注释类型声明中的元素不能声明形式参数
  • 接口抽象方法不能有正文
显然,Java语言目前不允许这样做。这可能是因为有很好的设计理由不允许它,或者,对于静态接口方法,“没有令人信服的理由这样做;一致性不足以改变现状”

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

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

在技术上是否可以将静态行为直接封装在注释类型中,以及如何封装?在JVM中实现这一点并与标准Java代码进行互操作在技术上是可行的,但需要注意的是:

  • 根据JLS,与Java兼容的源代码不能在注释类型中定义静态方法
  • 如果这些方法存在,Java源代码似乎能够使用它们,包括在编译时和通过反射在运行时
  • 主题注释可能需要放在单独的编译单元中,以便IDE和javac在处理代码时可以使用其二进制类
  • 这已经在OpenJDK 10 HotSpot上得到了验证,但是观察到的行为可能取决于内部细节,在以后的版本中可能会发生更改
  • 在决定采用这种方法之前,要仔细考虑对长期维护和兼容性的影响。
  • 概念验证是成功的,它使用了一种直接操纵JVM字节码的机制。 机制很简单。使用另一种语言或字节码操作工具(即ASM),它将发出一个JVM
    *.class
    文件,该文件(1)与合法Java(语言)注释的功能和外观相匹配,(2)还包含带有
    静态
    访问修饰符集的所需方法实现。这个类文件可以单独编译并打包到一个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!
        }
    }
    
    首先,将带有“normal”
    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
    文件中,或者使用自定义类加载器(未显示)将其直接加载到内存中:

    笔记:
  • 这需要生成字节码版本52(Java8)或更高版本,并且只能在支持该版本的JVM下运行
  • 注释的超级类型是
    java.lang.Object
    ,它们实现了
    java.lang.annotation.annotation
    接口
  • MethodNode构造函数的两个
    null
    参数用于泛型和声明的异常,在本例中均未使用
  • OpenJDK 10的HotSpot要求在静态方法上将
    MethodNode.annotationDefault
    设置为非空值(适当类型),即使在将注释应用于另一个元素时,设置/覆盖
    strlen
    永远都不是选项。这是一个灰色地带,因为这种方法是“合法的”。HS字节码验证器似乎忽略了ACC_静态标志,并假设所有定义的方法都是正常的注释
    ClassWriter cw = new ClassWriter(0);
    cn.accept(cw);
    byte[] bytecode = cw.toByteArray();