作者:Jason Hunter
了解如何使用 J2SE 5.0 中提供的元数据批注
最新的 J2SE 5.0 版本(也以代号 "Tiger" 著称)为 Java 语言引进了许多变化,这些变化旨在使 Java 编程更有表现力、更加开发人员友好和更安全。我在 2003 年 9 月的一篇题目为“Java 即将发生巨大变化”的文章中介绍了许多 Java 新特性。我没有介绍的一个重大的变化 — 那时还没有完整概述它 — 是 Java 的元数据工具。从本文开始,在一个新的分为四个部分的文章系列中,我将从一年前离开的地方继续向您显示如何充分利用 Java 的元数据。
在第一篇文章中,我将说明元数据的用途并演示如何使用在核心的 J2SE 资料库中提供的元数据批注。
在第二篇文章中,我将显示如何编写您自己的批注(首先编写类似 @Copyright 的简单的批注,然后看看与核心语言中内置的那些批注类似的更高级的批注)。
在第三篇文章中,我将演示工具如何在构建时使用批注(创建新的源文件或支持文件)以及程序如何在运行时使用批注(以改变代码的行为)。
在最后的第四篇文章中,我将介绍如何利用在 JSR-181 下开发的标准元数据批注的帮助使编辑和部署 Web 服务在未来变得更容易(Oracle 是 JSR-181 的专家组的成员,并且是在开发工具中增加对设计时元数据的支持的一个积极的拥护者。)
元数据
我承认当我第一次看到 JSR-175 的提案“用于 Java 编程语言的元数据工具”(在 2004 年 9 月发布;Oracle 也是该专家组的成员)时,我预测它将创建必须放在 JAR 的 META-INF 目录下的另一个属性文件,或者必须与 JAR 捆绑的另一个 XML 部署描述符。幸运的是,这不是元数据要做的。事实上,它正好相反。Java 的新的元数据工具提供了从 Java 代码内部批注 Java 代码的一种标准方式。它使您能够在要说明的元素的旁边放置描述性的元数据。
当讨论元数据时,您将经常看到几个类似的术语,因此下面提供了一个小术语表来帮助您了解它们的差异:
术语 | 定义 |
元数据 | 关于数据的数据。JSR-175 的目标是在 Java 语言中提供元数据工具。 |
批注 | 一种特殊的 Java 结构,用来修饰类、方法、字段、参数、变量、构造器或包。它是 JSR-175 选择用来提供元数据的工具。 |
批注类型 | 具有特殊实施的各种命名批注 |
属性 | 由批注指定的一个特殊的元数据项目。有时可以和批注交替使用 |
例如:富士苹果有一个属性:它是红色的。假定有一个 FujiApple 类,您可以使用 @Color 批注类型的一个批注来指定它的颜色。通过这么做,您就提供了关于苹果的元数据。
自 1.0 版以来在 Java 中一直存在对元数据的需求。Java 从来没有提供记录元数据的标准机制,因而我们编程人员找到了各种技巧和窍门使用任意的工具来添加元数据。您看到在 J2SE 1.4 和更低版本中使用元数据的一些地方有:
- transient 关键字
- Serializable 标记接口
- SingleThreadModel servlet 接口
- web.xml 部署描述符内部的元素
- META-INF/MANIFEST.MF 文件
- BeanInfo 接口
- @deprecated Javadoc 注释
- 所有的 XDoclet Javadoc 标记。
当使用这些技巧时,您可能没有想到您正在添加元数据,但事实上您的确在添加元数据。上述方法存在的问题是它们都是解决同一问题的不同方法,但通用性不好。每一种方法都至少有一个缺点在新的元数据工具中得到了解决。
对于这个列表中的一些方法,局限很明显。使用关键字不能伸缩;您不能使用用户自己定义的关键字。标记接口没有提供除它们的存在性之外的任何信息(即,它们没有带参数),并且它们只能处理类,而不能处理字段或方法或包。
列表中的其他一些方法可能看起来合理。使用 XML 支持文件似乎是个好主意,而事实上在许多情况下仍是个好主意。但对于我们使用 XML 文件的许多用途,例如指示类的哪一个方法应当看作是 web 服务,在 Java 代码内部将规则直接放在方法的旁边将更加高效。利用元数据,您可以使 XML 描述符文件仅包含与部署相关的决策。
该列表中可能最高效的元数据的用法是 @deprecated Javadoc 注释和在其镜像中创建的 XDoclet 标记。这可能是 JSR-175 语法为什么看起来与 @deprecated 标记非常类似的原因(正如我们将在下一部分中看到的那样)。
批注
批注可以很容易地附加到代码结构上。您可以写一个 "at" 符号 (@),然后是批注类型名称,并将批注直接放在要批注的项目前面。下面是一个简单的例子:
import javax.jws.WebService; import javax.jws.WebMethod; @WebService public class HelloWorldService { @WebMethod public String helloWorld() { return "Hello World!"; } }
当部署在正确的环境中时,增加 @WebService 和 @WebMethod 批注将指示 web 服务环境将该类变为 web 服务。
您可以批注方法、类、字段、参数、变量、构造器甚至整个包(利用一个特殊的外部 package-info.java 文件)。批注可以在括号内带任意数量的命名参数。下面是使用批注进行修饰以创建 web 服务的一个更高级的示例类。它包含了一个理论上的 JNDI 环境变量查找:
@WebService( name = "PingService", targetNamespace="http://acme.com/ping" ) @SOAPBinding( style=SOAPBinding.Style.RPC, use=SOAPBinding.Use.LITERAL ) public class Ping { public @env double level = 500.0; // JNDI lookup public @WebMethod(operationName = "Foo") { void foo() { } } }
这个例子显示了附加到类、变量和方法(在类上实际上有两个方法)上的批注。@env 批注没有任何参数,因此它不需要括号。其他批注有一个或更多的命名参数。
当创建新的批注类型时,您将限定允许哪些参数名以及它们的类型。批注接受的类型是严格限定的;它们只可以是基本类型、String、Class、枚举类型、批注类型和前面这些类型的数组。传递的参数必须始终是非空的编译时常量。
要了解本示例中显示的批注有什么效果必须等到本系列的第四篇文章。让我们开始看看 J2SE 5.0 提供的简单的批注类型:@Override、@Deprecated 和 @SuppressWarnings。
内置的批注
当我们看这三种标准的用户级批注时,必须考虑:在可以提供的所有可能的批注类型中,为什么 Tiger 恰恰提供三种?原因是提供大量的标准批注并不是目标所在。
JSR-175 的宗旨严格规定了它是要定义一个元数据工具。编写自定义批注类型的任务留给了编程人员,而编写一组标准的批注类型的任务留给了其他 JSR。例如,有一个新的名称为“Java 平台的通用批注”的 JSR-250,其宗旨是“为 J2SE 和 J2EE 平台中的通用的语义概念开发适用于各种技术的批注”。JSR-250 计划在 2005 年春天的某个时候在 javax.annotations 程序包中提供它的标准的批注集。还有之前提到的 JSR-181,它将使得在 J2EE 容器中编写 Web 服务变得更容易(我们将在本系列中的第四篇文章中进行介绍)。事实上,大多数新的企业 JSR(从 Servlets 2.5 到 EJB 3.0 到 JDBC 4.0)都在考虑批注可以提供哪些优点。
@Override
第一个 J2SE 标准批注 @Override 使您能够在代码中增加新的可选的编译器检查。它在方法中存在表示该方法用于覆盖父类中的方法。如果编译器检测到该方法实际上没有覆盖任何东西,那么将出现编译错误。经常使用,@Override 可以帮助您避免当方法标记没有完全匹配时 — 当多态变为(您可以称之为)“单态” ("unimorphism") 时 — 将得到的细微的 bug。
例如,以下代码可能看起来很合理:
public class OverrideExample { @Override public boolean equals(OverrideExample obj) { return false; } }
然而,当您编译 OverrideExample.java 时,您将得到一个错误,该错误指示一个细微的问题。
% javac OverrideExample.java javac OverrideExample.java OverrideExample.java:3: method does not override a method from its superclass @Override ^ 1 error
通过提示编译器您希望进行覆盖,使编译器能够捕获到 equals() 方法带 Object 类型参数的细微 bug。
@Override 批注在实际中有用吗?只有当您是一个愿意用 @Override 来标记每一个覆盖方法的非常严谨的编程人员时才有用。我们中有多少人能声称可以达到这种严谨程度?我认为我不能。可能 IDE 将找到一种方式来鼓励或强制使用 @Override。
@Deprecated
第二种标准批注是 @Deprecated,它与 @deprecated Javadoc 标记有几乎相同的行为。您可以用类似以下的方式来使用它:
public class DeprecatedExample { @Deprecated public static void badMethod() { } } public class DeprecatedUser { public static void main(String[] args) { DeprecatedExample.badMethod(); } }
The @Deprecated 批注看起来非常像 @deprecated 标记,除了它出现在注释外面的方法或类声明的前面,并且有一个大写字母 "D"。如果您试图编译上面的代码,javac 将产生警告:
% javac Deprecated*.java Note:DeprecatedUser.java uses or overrides a deprecated API. Note:Recompile with -Xlint:deprecation for details. 1 error
如果您遵循警告的建议并用 -Xlint:deprecation 进行编译,那么您将得到关于警告的详细信息:
% javac -Xlint:deprecation DeprecatedUser.java:3: warning: [deprecation] badMethod() in DeprecatedExample has been deprecated DeprecatedExample.badMethod();
@Deprecated 批注比 @Override 更有用吗?我不这么认为。该批注不支持任何参数,因此与 Javadoc 标记不同,您不能提供一个字符串来说明不赞成使用该方法并推荐一个替代的方法进行使用。@Deprecated 批注提供的价值实际上比 @deprecated 标记少。该批注唯一的优势是您可以通过编程的方式在运行时检测不赞成使用的项目。因此,传统观点认为应当同时使用 @deprecated 标记和 @Deprecated 标记,一个用于文档,另一个用于运行时反射。
我觉得很不幸 JSR-175 没有选择对 @Deprecated 做更多的工作。至少该批注应当复制 @deprecated 标记的功能,包含一个字符串说明,从而编译器可以将其与“不赞成使用” (Deprecation) 警告一起输出。利用额外的参数,@Deprecated 还可以接收 "isError" 布尔类型参数,以指示是否完全不鼓励使用该方法或者使用它将被认为是编译错误(利用解释错误原因的清楚的自定义说明来进行完善)。查看 C# 的示例 1 找到属性 [Obsolete],该属性正好实现了这一点,它被证明非常有用。
@SuppressWarnings
J2SE 提供的最后一个批注是 @SuppressWarnings。该批注的作用是给编译器一条指令,告诉它对被批注的代码元素内部的某些警告保持静默。
一点背景:J2SE 5.0 为 Java 语言增加了几个新的特性,并且和它们一起增加了许多新的警告并承诺在将来增加更多的警告。您可以为 "javac" 增加 -Xlint 参数来控制是否报告这些警告(如上面的 @Deprecated 部分所示)。
默认情况下,Sun 编译器以简单的两行的形式输出警告。通过添加 -Xlint:keyword 标记(例如 -Xlint:finally),您可以获得关键字类型错误的完整说明。通过在关键字前面添加一个破折号,写为 -Xlint:-keyword,您可以取消警告。(-Xlint 支持的关键字的完整列表可以在 javac 文档页面上找到。)下面是一个清单:
关键字 | 用途 |
deprecation | 使用了不赞成使用的类或方法时的警告 |
unchecked | 执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型。 |
fallthrough | 当 Switch 程序块直接通往下一种情况而没有 Break 时的警告。 |
path | 在类路径、源文件路径等中有不存在的路径时的警告。 |
serial | 当在可序列化的类上缺少 serialVersionUID 定义时的警告。 |
finally | 任何 finally 子句不能正常完成时的警告。 |
all | 关于以上所有情况的警告。 |
@SuppressWarnings 批注允许您选择性地取消特定代码段(即,类或方法)中的警告。其中的想法是当您看到警告时,您将调查它,如果您确定它不是问题,您就可以添加一个 @SuppressWarnings 批注,以使您不会再看到警告。虽然它听起来似乎会屏蔽潜在的错误,但实际上它将提高代码安全性,因为它将防止您对警告无动于衷 — 您看到的每一个警告都将值得注意。
下面是使用 @SuppressWarnings 来取消 deprecation 警告的一个例子:
public class DeprecatedExample2 { @Deprecated public static void foo() { } } public class DeprecatedUser2 { @SuppressWarnings(value={"deprecation"}) public static void main(String[] args) { DeprecatedExample2.foo(); } }
@SuppressWarnings 批注接收一个 "value" 变量,该变量是一个字符串数组,它指示将取消的警告。合法字符串的集合随编译器而变化,但在 JDK 上,可以传递给 -Xlint 的是相同的关键字集合(非常方便)。并且要求编译器忽略任何它们不能识别的关键字,这在您使用一些不同的编译器时非常方便。
|
public class DeprecatedUser2 { @SuppressWarnings({"deprecation"}) public static void main(String[] args) { DeprecatedExample2.foo(); } }
您可以将单个数组参数中的任意数量的字符串值传递给批注,并在任何级别上放置批注。例如,以下示例代码指示将取消整个类的 deprecation 警告,而仅在 main() 方法代码内取消 unchecked 和 fallthrough 警告:
import java.util.*; @SuppressWarnings({"deprecation"}) public class NonGenerics { @SuppressWarnings({"unchecked","fallthrough"}) public static void main(String[] args) { Runtime.runFinalizersOnExit(); List list = new ArrayList(); list.add("foo"); } public static void foo() { List list = new ArrayList(); list.add("foo"); } }
@SuppressWarnings 是否比前两个批注更有用?绝对是这样。不过,在 JDK 1.5.0 版本中还没有完全支持该批注,如果您用 1.5.0 来尝试它,那么它将类似无操作指令。调用 -Xlint:-deprecation 也没有任何效果。Sun 没有声明什么时候将增加支持,但它暗示这将在即将推出的一个 dot 版本中实现。
更进一步
如果您试图在 Javadocs 页面中查看这些属性,那么您可能很难找到它们。它们位于核心的 java.lang 包中,但有点隐蔽,它们出现在 Javadoc 类的最底端,列在 Exceptions 和 Errors 后面。
注意到了附加在 SuppressWarnings 批注后面的陌生的批注 @Target 和 @Retention 了吗?这些称为元数据批注,它们描述了该批注在哪里适用。我将在本系列的第二篇文章中介绍它们,以及介绍如何将元数据批注应用到您自己的批注中。
Jason Hunter 撰写了《Java Servlet 编程》,并与他人合著了《Java 企业最佳应用》(两者都由 O'Reilly 出版)。他是 Apache 的成员,作为 Apache 派驻 Java 团体发展进程执行理事会的代表,他缔结了 Java 开放源代码的里程碑式协议。他是 Servlets.com 和 XQuery.com 的发起人、Apache Tomcat 的最早的贡献者、com.oreilly.servlet 库的创始人,以及负责 Servlet、JSP、JAXP 和 XQJ API 开发的专家组的成员。他是 JDOM 库的创始人之一,该 JDOM 库实现了优化的 Java 和 XML 集成。在 2003 年,他获得了 Oracle 杂志的年度编辑奖。