【问题标题】:Can an annotation type define static methods?注释类型可以定义静态方法吗?
【发布时间】:2018-11-21 09:36:56
【问题描述】:

我已经开发了一个框架和相应的 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 运行时环境 18.3(内部版本 10+46)

  • 此处不允许使用静态修饰符
  • 注解类型声明中的元素不能声明形参
  • 接口抽象方法不能有主体

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

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

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

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

【问题讨论】:

  • 这似乎不是问题,这个问题的唯一理由是通过接口和注释的比较(类似于“接口和抽象类之间的差异”)。尽管 JVM 可能支持它,但您的回答(在您的编辑之前)是有偏见的并且没有解释 为什么 它应该被支持,而是它是可能的。这似乎是一个 XY 问题,因为它没有说明 why 对象需要通过注释来命名,因此我们无法建议适当的解决方法。

标签: java annotations jvm static-methods jls


【解决方案1】:

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

  1. Java 兼容的源代码,根据 JLS,不能定义注解类型中的静态方法。
  2. Java 源代码似乎能够使用这些方法如果它们存在,包括在编译时和在运行时通过反射。
  3. 主题注解可能需要放置在单独的编译单元中,以便在处理代码时它的二进制类可供 IDE 和 javac 使用。
  4. 这已在 OpenJDK 10 HotSpot 上得到验证,但观察到的行为可能取决于内部细节,可能会在以后的版本中发生变化。
  5. 在决定采用这种方法之前,请仔细考虑对长期维护和兼容性的影响。

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

机制很简单。使用替代语言或字节码操作工具(即 ASM),它将发出一个 JVM *.class 文件,该文件 (1) 与 合法 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 永远不会是一个选项em>将注释应用到另一个元素。这是这种“合法”方法的灰色地带。 HS 字节码验证器似乎忽略了 ACC_STATIC 标志,并假定所有定义的方法都是正常的注释元素。

【讨论】:

  • 您回答了自己的问题,但答案感觉非常基于意见。您假设该语言由于未指定的原因不支持此功能 - 它目前不支持它,但将来应该/将。
  • 我会在你的回答中质疑一些陈述。 “虽然注释与普通接口共享相同的一般抽象” - 这根本不是真的。接口抽象行为,而运行时注释似乎抽象状态(带注释的成员不能指定该注释的用户访问的行为 - 他们只能指定值)。 “它们不存在似乎没有技术原因” - 注释用于元数据:指定有关程序数据的数据。接口供用户与对象交互,同时不了解其实现。
  • 现在我们只是在争论不同的解释。 JLS 本身将注释和接口组合在一起引用和引用的句子指出它们都是“接口类型”......所以它们确实共享一个共同点(阅读:一般)抽象在某个级别。其次,我演示的方法是声明事实它们可以在运行时存在于未修改的JVM中,这支持了我的“技术原因”声明。您正在争论以这种方式使用是否合适,或者是否应该应该,这是公平的批评,但既不是我的论点也不是我的问题。
  • 电视上的按钮和语音命令都是两种类型的界面。汽车的方向盘也是如此。在谈论接口时,我们以相同的抽象来看待它们。但这并不意味着它们都是相同的,并且应该支持相同的功能。仅仅因为两个事物可以存在于同一抽象级别上,并不意味着当从更具体的意义上考虑这些抽象时,它们应该支持相同的特性。你确定你不是 XY 问题的受害者,会让人联想到代码异味吗?
  • (续)我的意思是,您的回答更多地是说明它们为什么应该存在的理由——应该在您的问题中提及。这不是为什么它不存在(为什么不支持)的答案。既然你们都提出并回答了这个问题,感觉就像是在努力宣扬信念,而不是给出答案。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-03-15
相关资源
最近更新 更多