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

时间:2018-06-12 05:01:47

标签: java annotations jvm static-methods jls

我开发了一个框架和相应的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接口类型上引入静态方法时(参见JLS 9.4),该功能被吹捧为能够提供...

  

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

     

- 来自 Java教程 Interface Default Methods

这已经在JDK库中用于提供诸如List.of(...)Set.of(...)等的实现,而以前这样的方法被降级为单独的实用程序类,例如java.util.Collections。通过在相关接口中找到实用程序方法,它improves their discoverability并从API域中删除可能不必要的辅助类类型。

由于注释类型的当前JVM bytecode representation与普通接口密切相关,我想知道注释是否也支持静态方法。当我将助手移动到注释类型时,例如:

@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
  •   
  • 注释类型声明中的元素不能声明形式参数
  •   
  • 界面抽象方法不能有身体
  •   

显然,Java语言目前不允许这样做。可能有很好的设计理由不允许它,或者previously presumed用于静态接口方法,"没有令人信服的理由这样做;一致性并不足以引起人们改变现状"。

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

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

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

1 个答案:

答案 0 :(得分:1)

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

  1. 根据JLS,与Java兼容的源代码无法在注释类型中定义静态方法。
  2. Java源代码似乎能够使用 这些方法,包括在编译时和运行时通过反射。
  3. 主题注释可能需要放在单独的编译单元中,以便在处理代码时,IDE和 javac 可以使用其二进制类。
  4. 这已在OpenJDK 10 HotSpot上得到验证,但观察到的行为可能依赖于内部细节,可能会在以后的版本中发生变化。
  5. 在决定采用这种方法之前,请仔细考虑对长期维护和兼容性的影响。
  6. 概念验证成功,使用直接操作JVM字节码的机制。

    机制很简单。使用替代语言或字节码操作工具(即ASM),它将发出JVM *.class文件,其中(1)匹配 legal Java(语言)的功能和外观注释,以及(2)还包含使用static访问修饰符集的所需方法实现。这个类文件可以单独编译并打包成JAR或直接放在类路径上,此时它可以被其他普通的Java代码使用。

    以下步骤将创建对应于以下 not-quite-legal 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!
        }
    }
    

    首先,将带有“普通”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文件,或使用自定义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设置为静态方法的非空值(相应类型),即使设置/覆盖strlen永远不会注释应用于另一个元素时的选项。这是一种“合法”的灰色区域。 HS字节码验证器似乎忽略了ACC_STATIC标志,并假设所有定义的方法都是正常的注释元素。