Mike Keith 开发人员:J2EE

是否使用批注?
作者:Mike Keith

详细了解批注以及将批注和 XML 作为一种元数据语言使用的具体用例

Java 2 标准版 (J2SE) 5.0 现在成为现实,批注变得流行起来,它引进的 Java 元数据特性被许多宣传为一种开发应用程序的更好方式与完全的编程革命的中间物。无论它是什么,无疑都值得探索和仔细研究一番,确切了解它如何优于当前我们都了解并喜爱或深爱的元数据技术 (XML)。

本专栏对存储和读取元数据的两种范例进行了一些比较,并考虑了批注的优缺点。代码示例帮助具体解释了其中的一些问题。

我假定您熟悉批注和程序元数据。不熟悉的读者请访问 Java 规范请求 (JSR) 页面,阅读有关 JSR-175 的内容。事实证明,要更好地掌握批注,该规范的公众审议草案要比最终版本对你将更有帮助。最终版本为了将批注特性集成到了该语言中,修改了所有相关的 VM 和语言规范部分以将批注包括进去。结果是在很多不同的文档语境中产生了许多批注片断,这些批注片断很难顺序使用。

房地产原则

人们在购置房地产时常常强调的三要素“位置、位置和位置” — 用在这里再恰当不过了。批注和 XML 之间最主要的差别是元数据的位置。它们的优缺点大部分由元数据的放置所导致,其影响远比表面看到的要深远。

XML 元数据通常存储在平面文件中。虽然有可能利用文件结构(如目录层次结构)提供更多的语境,但很少这么做。更多的时候,XML 利用在每一种情况下显式提供的描述主题随机组合在一起成为一堆元数据。

比如,允许将对象远程化(无需实施任何特殊的“远程”接口)并且还允许将某些方法选择性地标记为远程的中间件层。此外,当调用为本地调用时,它允许将方法参数指定为按引用传递。要在 XML 中指定这一点,可以应用以下 XML 段:

<remote>
<package name="com.acme"> 
<class name="InventoryControl">
<method name="addNewItem">
<method-params>
<method-param type="Integer"/>
<method-param type="String">
< pass-by-reference/>
</method-param>
<method-param type="Integer"/>
</method-params>
</method>
</class>
</package>
</remote>

我发现这是一个相当冗长的例子,大多数人都认识它可能不是指定单个方法的最佳 XML 结构。相反,您可以试着做其他一些事情,如完全限定类名或更多地利用属性:

<remote>
<method name="addNewItem" 
class="com.acme.InventoryControl">
<method-params>
<method-param type="Integer"/>
<method-param type="String" 
passByReference=true/>
<method-param type="Integer"/>
</method-params>
</method>
</remote>

这个例子在更现实的情况下(这时每一个类需要多段元数据)将受到抨击。在这种情况下,属性必须在每一个 XML 元素中重复,而重复的元素是低劣的 XML 设计的一个症状。这个问题再次牵扯到为唯一指定要调用的方法而对语境即程序包、类、方法和参数的需求。虽然指定 XML 的方法不在本讨论的范围之内,但认识到无论采用什么样的结构来从语境上区分元数据,均需要提供更多语境信息这一点就足够了。

另一方面,批注被添加到它们所描述的程序产物中。这在查看元数据时提供了与元数据的关联性,同时提供了必须以 XML 显式传达的多层语境。上述例子类似的批注可简单地描述为

public class InventoryControl {
@Remote
public void addNewItem(Integer itemId, 
@PassByRef String description, 
Integer count) { ... }
}

但这是否真的类似?在中间件层必须确定启动方法是什么的时候,情况将如何。它将如何确定该类中的方法是否是可远程访问的?处理器为了找到包含批注的一个方法必须搜寻每个应用程序类的每个方法,这是否合理?看起来似乎正是利用了 Java 语境这一事实才导致了查找元数据的困难,这是因为没有显式的指示引导处理器找到它。嗯,就像 XML 的例子由于仅有一段元数据看起来有点不现实,这个例子也有点不太规范。通常的情景是多个程序产物在多个层上需要元数据,因此就像 XML 语境可能需要指定一个类元素以便它的所有类元数据都可以集中在一起一样,您也可以期望用一些东西来为类添加批注。

@Remotable
public class AcmeStartup {
@Remote
public void addNewItem(Integer itemId, 
@Optimize String description, 
Integer count) { ... }
}

我认为哪种方法更不冗长、更易于指定并能够更有效地进行表达是很显然的。

将元数据与源代码耦合的其他优点完全体现在实际应用中。例如,如果应用程序需要修改 addNewItem 方法以接收一个额外的 backOrder 参数,那么应用程序或修改 addNewItem 方法的实体还必须记得修改 XML 文件,该文件可能存储在与类完全不同的位置中。之所以需要记得修改 XML 文件,只是因为这些方法参数是指定该方法所需的语境 XML 信息的一部分。那么,使用批注的维护动机就相当明显了。

类似地,没有与代码连接的 XML 还不是与代码相同的版本受控的元素的一个集成的部分。修改一个元素和创建它的一个新版本本质上并不意味着将创建另一个元素的一个新版本,尽管可以配置一些版本控制系统来实现此目的。虽然存在修改一个元素而不要求修改另一个元素的情况,但即使一个方向(即,代码上的元数据)上的依赖性也指示两者需要更恰当的耦合。

面对结果

那么简洁的代价是什么?在源代码旁而不是在一个取消耦合的 XML 文件中指定元数据会带来一些影响。

以一个将元数据添加到现有类中的工具为例。在该过程的第 1 步,我们立即遇到了第一个最明显的问题。如果只提供了类文件而没有提供源代码那该怎么办?可能只在运行时读取批注,而不会添加批注。这时不能选择为类添加批注,该工具被强制使用 XML 或类外的一些其他机制。

下一个步骤才实际添加批注。该步骤成为该工具工作量非常集中的过程,这是因为该过程需要分析源代码并在正确的位置添加新批注。尽管使用 XML 只是简单地指定语境,然后根据给定的模式写出 XML,而我们现在必须找到元素的源代码,分析它,添加语法正确的批注段,然后将其全部重写回去。这说明它是非常占用资源的。

一旦添加了批注,就需要重新编译类。要实现此目的,插入到源代码中的批注的定义需要包含在类路径中。虽然对于立即重新编译这可能不是问题,但在以后这可能产生问题。批注(与 XML 文件类似)喜欢处在它们被完全忽略的环境中。如果一开始添加批注的处理层使用运行时反射作为解释元数据的一种方式(这是典型的情景),那么明显需要将批注保留在类文件中,直到运行时为止。幸运的是,在类加载包含定义不在类路径中的批注的元素时 VM 比较包容。如果类似地放松编译时的依赖性并简单地忽略任何未找到接口的批注(拥有纯粹的 SOURCE RetentionPolicy 的语法,即不在类文件中保留批注),那么将很好。与批注接口定义等价的 XML 产物是模式,只有在读取 XML 文件时才需要它。

元数据编程

如果您将批注比喻成一次政变,那么该政变后将没有能干的或训练有素的政府准备接管政权。对于元数据编程,批注目前处于不成熟的状态。缺少一些显然应该具有的特性,而其他特性也不足以满足日常使用。I discuss some of these below:

继承。没有。如果有批注类型继承那将很好,但对批注类型继承的支持还没有提供。例如,如果能够定义如下的批注类型层次结构那么将很好:

public @interface Event {
String debugString() default "";
}

public @interface SyncEvent extends Event {
boolean allowPreempting();
}

public @interface AsyncEvent extends Event {
boolean joinAfterCompletion();
}

如果批注接口可以扩展其他接口,可以使用 SyncEvent 或 AsyncEvent 来批注一个事件方法;并且在启用调试时可以指定将一个可选的调试字符串打印到事件日志中。那么,就像在代码中一样,如果需要另一个公共成员,您就可以将公共成员添加到 Event 超级接口中来轻松添加它。否则,您必须选择一种笨拙的替代方法。显而易见的方法是在两个事件中重复使用该成员。这产生了一个潜在的维护问题,并违背了被普遍接受的应避免克隆代码的软件实务。另一种选择是创建一个拥有公共成员属性的公共批注,如下:

public @interface EventOptions {
String debugString() default "";
}

public @interface SyncEvent {
EventOptions options() default @EventOptions;
boolean allowPreempting();
}

public @interface AsyncEvent {
EventOptions options() default @EventOptions;
boolean joinAfterCompletion();
}

我也发现这种方法不是最优的,这是因为它现在要求源代码必须嵌套调试字符串和 EventOptions 批注中的其他任何公共选项,如下:

@AsyncEvent(
options=@EventOptions(debugString="myEventMethod called"),
joinAfterCompletion=false)
public void myEventMethod(EventDescriptor eventDesc) { ... }

您可能会提出您自己喜欢的变通方法,但唯一的优秀解决方案是支持批注类型的继承。

请注意,即使批注类型可能不是在继承层次结构中构建的,仍然可以将批注应用到普通的类继承树上。批注的反射 API 提供了在继承的程序元素上访问批注的能力,这很像其余的反射 API 可以在属于继承结构的类上进行操作一样。

默认值。值得庆幸的是,J2SE 5.0 对提供默认值给予了一定的支持,但目前还很不方便并且不充分。因为不允许将空值作为有效的默认值,所以需要为每种类型保留一个特殊的“未初始化”的值。如果不能实现这一点,那么当读取批注时很难知道默认值是实际指定的还是获得的(即不是指定的)。在这些类型的值之间通常有语法差异。因为默认值必须是实际的批注,因此对于嵌套的批注,情况变得更糟。令人不快的变通办法是创建一个虚假的批注成员,它只是用来区分批注是指定的还是默认提供的。

public @interface EventOptions {
String debugString() default "";
boolean specified() default true;
}

public @interface SyncEvent {
EventOptions options() default @EventOptions(specified=false);
boolean allowPreempting();
}

在这个例子中,在 EventOptions 为元素添加批注时,实际上从未使用指定的成员。它只被 SyncEvent 用来为将 EventOptions 批注默认为未指定,然后由批注处理代码用来确定是否获取了批注值(因为它可能是指定值或默认值):

SyncEvent event = element.getAnnotation(SyncEvent.class);
if (event.options().specified()) {
printDebug(event.options().debugString());
} else {
// 未做任何指定
}

上面产生的示例事实证明不会比 XML 的例子(其中只能为简单的类型指定默认值)差。目前还不存在的一个更有用的特性是当默认值实际依赖于一些程序元素时,默认值应当是可参数化的并能够引用相同的程序范围内的其他元素。例如,如果您想使默认值引用一个能够被管理工具修改的静态变量,那该怎么办?或者,如果能够使一个批注值默认为它所批注元素的名称呢?虽然默认值可参数化的能力将确实为处理器提供帮助,但实际上这将非常难以实现,这是因为批注类型在定义时没有任何实际的范围。可能需要保留字(如 this)来使默认值可参数化 — 一个可实现的目标。

允许单值批注类型意味着如果成员的名称是魔法字 (magic word)“value”,那么这个名称可以跳过(默认为 value)。虽然这有一些帮助,但相信它是一个相当随意和不必要的选择。如果使用单成员批注,那么不是很明显为什么在任何情况下都不能将其作为一个“value”成员(而不实际称其为“value”)对待。因为确保名称仅在单成员批注内有用,所以它似乎没有任何实际的用途,而将更具说明性的名称赋予单成员批注的能力将非常有用。

验证。允许的成员值类型的集合严格受限并且必须遵循语法规定,而简单的值类型验证基本上是自由的,就像 XML 分析器检查基础 XML 模式类型的类型定义一样。此外,一种 @Target 批注提供了一种由 VM 进行的方便的补充检查,以确保批注类型不会批注不恰当的程序元素或不应由该批注类型批注的元素。然而,不执行大量的代码检查和/或批注返工要限制可能共同批注一个元素的批注将更加困难。对于一种方法来说,既为 AsyncEvent 又为 SyncEvent 将没有意义,但怎么阻止人们以这种方式进行批注呢?处理器将不得不寻找两种批注并对一种方法上存在的两种批注的异常情况进行错误检查。可以重新修改批注使其使用枚举类型,从而限制可以指定的内容。继承可以在某种程度上解决这个问题,这是因为您至少可以引进一个包含一个单事件成员的父元素,从而将元素限制为仅为单个事件。然而,真正有帮助的是一种附加的内置批注,它允许批注定义将一组批注指定为在批注同一编程元素时不可接受的批注对。

命名空间。在批注领域中要考虑的一件事是,多层批注在同一全局批注命名空间中可能发生冲突。例如,一些公共的批注类型名称(如 @Transaction)可能被多个层用来表示每个层特有的一些稍有不同的语义。我同意这是一种合理的考虑,因为批注的优势之一就是潜在的简洁性(潜在是因为它们不一定是简洁的;要使其简洁,必须对其进行设计),而使名称变长以减少冲突的可能将削弱这种优势。

避免这种境地的策略之一是组成诸如 JSR 250 之类的组,这些组将试着定义一组可赋予特定语义的公共批注。一组标准化的批注将有望避免每个人定义他们各自本质上可能表示相同含义的批注,但如果它们实际上的确有不同的含义,那名称将有望正确地区分它们自己(并尽可能地简洁)。标准组如 JSR 181、JSR 244、JSR 220 以及其他正在定义批注的组之间的协作也将非常有帮助。

批注命名空间问题最终类似于类命名空间问题,作为最后的手段,我们可能将不得不完全限定批注。这似乎骇人听闻,但愿仅在紧急冲突时才使用,但在需要它的情况下将无法避免:

@ com.acme.AsyncEvent(joinAfterCompletion=false)
public void myEventMethod(EventDescriptor eventDesc) { ... }

再论位置

批注的概念(添加有关对象的额外信息的一种方式)意味着必须有一个对象来为其添加信息。困难之一是一些元数据比任何特定的程序元素都拥有更大的全局范围,这意味着实际上没有合适的对象来批注或定位元数据。迄今为止这个领域中唯一的解决方案是 package-info.java 文件,它允许在给定的程序包上进行批注。程序包级的批注位于这个文件中,但没有关于如何指定全局元数据的提示。人们可能期望在应用程序级使用 XML,这么做除了如果这是需要使用 XML 的唯一理由,并单单为了这个目的不得不引入 XML 的所有多余的东西外,也没有什么不可以的。

结论

总而言之,批注仍然存在很多问题,一些人仍打算继续使用 XML。实际上,批注永远不可能作为一种元数据语言替代 XML。不过,我相信存在其中一种方法肯定优于另一种方法的特殊用例。
后续步骤

阅读 JSR 175 规范的公共审议草案

Oracle 的 Debu Panda 谈利用 EJB 3.0 简化 EJB 开发

下载 OC4J 10g

教程:EJB 2.1 技巧

当元数据适于更加动态地进行配置或在部署时进行配置时,XML 是适于使用的语言。类似地,如果没有提供源代码而且需要由不同的人在过程的不同阶段递增地添加另外的元数据(与 Enterprise JavaBean [EJB] 角色之类的情况类似),则 XML 无疑也是最佳选择。

然而,当目标的确是要在源代码上添加元数据层并且这唯一地适用于特定代码元素时,Java 中的元数据可以很好地满足这一需求。能够在与代码相同的时间和地点提供元数据使开发变得更加容易。由于对象模型是由批注类型定义本身定义的,因此不仅开发变得更容易而且处理操作也简单多了。不需要其他文档对象模型 (DOM) 或其他模型了。此外文档编制也是集成的,内置的 @Documentation 批注将在生成的 javadoc 中包含批注类型。

批注将保留下来。它们已经在 Java 舞台上登场了,并不断被集成到各种 Java 规范和标准中。它们正被广泛采用并被依赖用来处理 J2EE 标准(如 EJB)中的标准化元数据,并且不断进军开放源代码组件,如 TestNG。

大多数的标准 Java 2 企业版 (J2EE) 应用服务器平台(如 Oracle Containers for J2EE (OC4J))将在即将推出的版本中提供一定程度的批注支持。这将在很多方面降低开发和部署时间,从而不断提升开发人员在 Java 平台上的生产效率。

免责声明

原来我在本专栏中提出的一些问题已在 JSR 175 规范的问答部分中进行了讨论,但我只是对 JSR 175 专家小组作出的决定所依据的理由,或者更确切地说,用例动机持有不同意见。这可能只是我自己对批注或使用模式期望的看法稍有不同,或者只是我的想法与批注规范小组的想法不同。

我对批注所作的所有工作都是基于 J2SE 5.0 测试版的。在生产版中可能存在一些差异。


Mike Keith 是 Oracle Containers for J2EE (OC4J) 和 TopLink 产品设计师,并且目前是 EJB 3.0 专家小组中的 Oracle 方的代表。

请为本文评定等级:

优秀 良好 一般 低于一般水平 较差


把您的意见发送给我们

寄送此页面
Printer View 打印机视图