充分利用 Java 的元数据,第 2 部分:自定义批注
作者:Jason Hunter
了解如何编写您自己的批注类型并使用内置的批注来控制它们的行为。
在我的本系列的前一篇文章中,我介绍了 Java 的新元数据工具以及内置的批注类型 @Override、@Deprecated 和 @SuppressWarnings。在本文中,我将介绍如何编写您自己的批注类型并使用 java.lang.annotation 程序包中的内置批注控制批注的行为。
一想到自定义批注,我脑海中就萦绕着几种想法。假设编写一个 @ThreadSafe 批注向任何调用方声明类或方法的线程安全设计,或编写一个 @NotThreadSafe 来用作警告。假设编写一个 @Copyright("Jason Hunter") 批注以在字节码中指定类文件的版权,还可以编写一个相应的 @License(License.APACHE20) 批注来指定共享代码的条款。与包含在注释中的版权和许可语句不同,这些批注可以在编译后保留并且可以通过编程方式进行记述。也许您有自己的想法,但无论如何都要进行大量的实验!本文将帮助开始这些实验。
@Unfinished 批注类型
在本文的示例中,我们将假设并创建一个 @Unfinished 批注类型。该示例的想法来源于这样一种常见情况:您只是编写了一个代码块的轮廓而某些类或方法暂时尚未完成 - 一个可在以后添加相应代码的基本结构。您或许像我一样,通常使用一些“XXX”或“TODO”注释(或其他易于搜索的注释)标记这些区域。使用 Java 元数据和自定义批注类型,您可以改用批注将这些代码结构标记为 @Unfinished。
该方法提供了几种有意义的可能性。例如,您可以在运行时让任何调用未完成代码的单元测试生成一个特殊的“unfinished”结果,从而与常规失败相区别。您也可以在编译时每当彻底完成的代码与未完成的代码存在相关性时输出警告,并将其标记为“effectively unfinished”。此外,由于注释通常可以采用任何格式,因此可以编写一个批注类型来接受描述字符串、可选所有者列表以及优先级(具有假定的默认值)。
使用特殊的 @interface 语法编写批注:
public @interface Unfinished { } // Unfinished.java@interface 很像批注但并不完全一样。可以将它视为一个伪批注,也就是说它只是将类标记为批注。正如以上注释所描述的,您将批注放在常规的 .java 源文件中。批注向下编译为常规的 .class 文件。
该批注缺少大量特性,因此现在通过自引用的方式将它标记为 unfinished(未完成):
@Unfinished public @interface Unfinished { }批注参数
要为 @Unfinished 批注类型提供它的描述、所有者和优先级,您将需要向批注声明中添加参数。批注参数遵循某些严格的规则:
- 参数类型只能为 primitive、String、Class、enum、annotation 或其中任意类型的数组。
- 参数值决不能为空!
- 每个参数可以声明一个默认值。
- 可以用缩写样式设置一个名为“value”的参数。
- 参数编写为简单的方法(没有参数、没有 throw 子句等)。
@Unfinished("Just articleware") public @interface Unfinished { public enum Priority { LOW, MEDIUM, HIGH } String value(); String[] owners() default ""; Priority priority() default Priority.MEDIUM; }关于这个简短示例,有几个要点需要注意。首先,我们已经使用一个表面类似方法声明的语法添加了参数:value()、owners() 和 priority()。您将在我的下一篇文章中看到,这些方法用作可在运行时可调用的 getter。
“value”参数的名称比较特殊,它只在属性传递一个参数时使用。这一点可以从“Just articleware”批注中看出。我们之所以必须添加该描述,是因为“value”参数未声明默认值,所以使用没有描述的批注将生成编译错误:
Unfinished.java:4:annotation Unfinished is missing value @Unfinished ^ 1 error其实我们可以通过为 value() 参数提供一个默认值(就像我们为 owners() 参数提供默认值一样)来允许不使用参数。注意 owners() 参数是怎样将一个简单字符串用作默认值,而该参数的类型为 String 数组。能这样做完全得益于 varargs(另一个 J2SE 5.0 特性)。(有关 varargs 以及用于定义 Priority 类型的新枚举工具的更多信息,请参见我馔写的前一篇文章。)
您可能觉得在声明中使用“default”有点奇怪,但实际上“default”关键字并不是一个新关键字,Java 以前曾在 switch 语句中使用过它。判断某种语言是否成熟就是看每个关键字是否有多种用法。
以下是一个演示 @Unfinished 参数特性的示例:
@Unfinished( value="Class scope", priority=Unfinished.Priority.LOW ) public class UnfinishedDemo { @Unfinished("Constructor scope") public UnfinishedDemo() { } @Unfinished(owner="Jason", value="Method scope") public void foo() { } }该参数的第一个用例将整个类标记为具有低优先级且没有指定所有者的 unfinished。第二个用例将构造函数标记为 unfinished 并且只提供了所需的描述,而未提供指定的所有者或优先级。最后一个用例将 foo() 方法标记为 unfinished 并提供了所有者和描述,同时将描述移动到第二个参数而非第一个参数以说明命名参数的顺序并不重要。
程序包级别的批注情况如何?由于没有特定的位置放置这些批注,因此将它们放置在专门指定的 package-info.java 文件中。如下所示:
// package-info.java @Unfinished("Package scope") package com.servlets;您需要包含 package 语句以指示将批注放在哪个程序包中。不能认为磁盘上的位置是可靠的。
要编译以上的 package-info.java 示例,@Unfinished 批注不能位于默认的程序包中。在 J2SE 1.4 之前,可以使用如下所示的语法导入默认程序包中的类:
import Unfinished;该语法已被禁止使用了。因此从封装的类中访问默认程序包类需要将默认程序包类移动到它自己的程序包中。因此,从现在开始,将 @Unfinished 移动到 com.servlets 程序包中:
package com.servlets; @Unfinished("Just articleware") public @interface Unfinished { ...现在,我们希望 @Unfinished 还遵循其他几个规则:它不应可附加到字段、参数或局部变量(它在这些位置中不起作用)。它应显示在 Javadoc 中。并且它应在运行时持续存在。想必您已经猜到,我们把这样的规则指定为批注。
批注的批注
J2SE 5.0 在 java.lang.annotation 程序包中提供了四个批注,这些批注仅在编写批注时使用:
- @Documented - 是否将批注放在 Javadoc 中
- @Retention - 何时需要批注
- @Target - 批注的目标
- @Inherited - 子类是否获得批注
@Documented
默认情况下,类或方法的批注不会出现在该类或方法的 Javadoc 中。@Documented 批注改变了这种情况。它是一个简单的标记批注并且不接受参数。使用 @Unfinished,我们希望用户知道哪些类和方法的工作尚未完成,因此我们将使用此元批注标记 @Unfinished:
package com.servlets; import java.lang.annotation.*; @Unfinished("Just articleware") @Documented public @interface Unfinished { ...注意新的 import 行以及位于同一级别的多个批注。可以在某个元素上放置多个批注,但不能放置两个相同类型的批注。进行此更改后,在前面的 UnfinishedDemo 示例的 Javadoc 中,您将看到以下内容:
![]() | ![]() |
---|
@Retention
您需要将批注保留多长时间?RetentionPolicy 枚举中列出了三个选项:
选项 | 注释 | 示例 |
---|---|---|
RetentionPolicy.SOURCE | 在编译过程中丢弃。编译完成后,这些批注将不起任何作用,因此不会将它们写入字节码。 | @Override、@SuppressWarnings |
RetentionPolicy.CLASS | 在类加载过程中丢弃。当执行字节码级的后期处理时很有用。有些另人吃惊的是,这是默认值。 | - |
RetentionPolicy.RUNTIME | 不丢弃,以便可以运行时用于反映。 | @Deprecated |
@Retention 批注使您可以为自定义批注类型指定所需的 RetentionPolicy;它接受一个 RetentionPolicy 类型的“value”参数。要使 @Unfinished 示例 RetentionPolicy.RUNTIME 更有意义,您可以进行以下更改。
package com.servlets; import java.lang.annotation.*; @Unfinished("Just articleware") @Documented @Retention(RetentionPolicy.RUNTIME) public @interface Unfinished { ...在我的下一篇文章中,我将介绍如何在运行时读取批注。
@Target
现在,您要将批注放在哪里?ElementType 枚举中列出了八个选项:
- ElementType.TYPE (class, interface, enum)
- ElementType.FIELD (instance variable)
- ElementType.METHOD
- ElementType.PARAMETER
- ElementType.CONSTRUCTOR
- ElementType.LOCAL_VARIABLE
- ElementType.ANNOTATION_TYPE (on another annotation)
- ElementType.PACKAGE (remember package-info.java)
package com.servlets; import java.lang.annotation.*; @Unfinished("Just articleware") @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD, ElementType.CONSTRUCTOR,ElementType.ANNOTATION_TYPE, ElementType.PACKAGE}) public @interface Unfinished { ...@Inherited
最后,@Inherited 控制批注是否影响子类。例如,@Unfinished 超类是否指示未完成的子类?或许指示,因此以下是 @Unfinished 的最终格式:
package com.servlets; import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD, ElementType.CONSTRUCTOR,ElementType.ANNOTATION_TYPE, ElementType.PACKAGE}) @Inherited public @interface Unfinished { public enum Priority { LOW, MEDIUM, HIGH } String value(); String[] owners() default ""; Priority priority() default Priority.MEDIUM; }后续文章
在本系列的下一篇文章中,我将介绍如何增强 Java 的反射功能以帮助您在运行时发现批注,并介绍了如何通过批注处理工具“apt”在构建时使用批注。