Classworking 工具箱: 注释与配置文件

发表于:2007-05-24来源:作者:点击数: 标签:Classworking配置文件工具箱注释
注释允许您将元数据指定为源代码的一部分。使用这个特性,可以将工具指令嵌入代码,而不是创建单独的配置文件(需要与源代码同时进行维护)。但是,根据 Java 咨询顾问 Dennis Sosnoski 的解释,配置文件仍然有它们的用处,尤其是对于那些横切应用程序源代码
注释允许您将元数据指定为源代码的一部分。使用这个特性,可以将工具指令嵌入代码,而不是创建单独的配置文件(需要与源代码同时进行维护)。但是,根据 Java™ 咨询顾问 Dennis Sosnoski 的解释,配置文件仍然有它们的用处,尤其是对于那些横切应用程序源代码结构的类似方面的函数。

注释是 J2SE 5.0 的一个主要的新特性,其设计目标是允许将关于代码的元数据定义为代码的一部分。使元数据与它引用的实际代码实现内联,从而提供引用的本地性,使所有事物都汇集在一个地方。在对应的代码发生变化时,这种方式可以更容易地维护元数据。但是,注释也有一些弱点。

除了将元数据与代码内联之外,还可以用其他方法定义元数据。基于“配置文件方式就是使用独立的非 Java 文件表达某种形式的元数据”这一认识,我针对这期专栏,在“配置文件”这个通用标题下将这些替代方法收集在一起。这种方式产生了潜在的维护问题,因为元数据必须引用物理上分离的代码。如果代码发生变化,那么就可能需要修改元数据,并且采用配置文件的方式表示元数据,您必须记得要找出单独的配置文件并修改它。

在这一期中,我将讨论我认为的这两种方式的利弊,并研究在将 Java 标准进程和许多开源项目转换成始终采用基于注释的方式表示元数据时,对开发人员有意义的东西。

历史背景

在开发 Java 平台之前,配置文件已经应用了很长一段时间,但是通常只是用来提供运行时参数,控制应用程序的执行。J2EE 平台和相关组件标准的开发使得另外一种配置文件得以广泛应用,这类文件包含框架用来处理应用程序代码的信息。最早的一个示例是用于 Java servlet 部署配置的 web.xml 文件。

Java 平台支持动态的类装入和通过反射在运行时访问类元数据,提供了许多不同的利用配置文件,在运行时控制程序装入的方式。开发人员很快就利用上了这种方式提供的灵活性。但是随着框架配置文件的泛滥,开发人员发现生成和维护这些文件太复杂。对 Java 源代码进行修改可能要求对配置文件也要进行修改,但是在源代码中没有任何迹象表现出这种需求 —— 相反,开发人员需要清楚源代码和配置文件之间的连接,并记得保持它们同步。

进入 XDoclet

流行的 XDoclet 工具的开发目的是帮助解决管理文件之间的这个连接问题。XDoclet 不以独立文件的方式维护配置信息,而是允许使用特殊的 Javadoc 标签在 Java 源代码中嵌入配置信息。使用 XDoclet,可以将配置信息与源代码保存在一起,这样需要跟踪的事情就都在一个地方了。

XDoclet 实际上不只是擅长生成配置文件。它最初的设计目标是用于 EJB,除了实际的配置文件之外,对于每个 bean,EJB 还需要多个样板文件。XDoclet 可以根据基本的 bean 源代码(受特殊的 Javadoc 标签控制)替 EJB 生成这些样板文件。随着 XDoclet 已经扩展成可以处理许多 EJB 之外的其他应用,这个代码生成功能也得到了扩展。在需要样板代码的地方,它是极为有用的工具,可以帮助源代码树消除那些对应用程序的操作毫无用处的混乱。

XDoclet 也有一些限制。因为它使用简单的转换将 Javadoc 样式的注释转换到配置文件,所以无法检测配置在许多方面的准确性。过去,我曾经由于拼写错误或者指定值的逗号不匹配,在使用 XDoclet 时有过不幸遭遇。这类错误会带入生成的代码或配置文件,从而导致难以回溯问题的根源。

用注释来营救?

受 XDoclet 的成功的部分影响,JSR-175 在 2002 年成形了 ,它提供了一个标准的机制,以任意属性信息的形式,将元数据与具体的 Java 类、接口、方法和字段关联起来。虽然在 JSR 草案中列出了与 XDoclet 类似的定制 Javadoc 标签,并且这可以作为一种可能的实现,但是 JSR-175 元数据支持的最终形式所采用的方式完全不同。该支持对 Java 语言定义注释的方式进行了扩展,在任何 Java 组件声明的前面,都可以添加一组类似 JavaBean 的名称-值对来作为修饰符。

注释消除了 XDoclet 方式的许多限制。通过使用类型化的值并允许将特定注释限制在只能用于某种类型的 Java 组件上,注释支持的验证级别比 XDoclet 提供的验证级别更高。因为注释集成在 Java 语言的定义中,所以处理它们更容易一些(只要正在使用 JDK 5.0 即可 —— 关于在早期的 JVM 中使用注释的一些指示,请参阅 上月的专栏)。从使用的角度来说,注释更加灵活,注释拥有一些选项,这些选项可以指定编译器的类文件输出中是否包含注释信息,是否允许应用程序在运行时使用这些信息。

从 JSR-175 出现开始,人们就对将元数据定义成其他 JSR 的组成部分很感兴趣。在作出在 J2SE 5.0 中包含注释的决定之后,注释的使用就成为许多计划在 J2SE 5.0 发布之前完成的新 JSR 的一部分。我们先从了解目前这些提交公众评论的 JSR 开始,然后,在下一节中,我将使用其中一个示例。





权衡

注释代表对于将配置信息嵌入 Java 源代码中的 XDoclet 规则的极大改进。这种方式的主要优势是:每件事都在一个位置上,配置信息直接与 Java 组件关联。由于这个原因,源代码的许多重构类型对于注释来说都是透明的,因为注释总是应用在它们附着的组件上,即使在组件移动或改名时也是如此。对于要求创建新注释或修改注释的其他重构来说,每件事都处于相同的位置,从而确保开发人员能够看到注释,并增加开发人员记得进行必要的修改的可能性。

在注释的优势之外,我看到了它们过度使用的两个主要不足。第一个不足是:源代码可能充斥着各种各样与实际程序逻辑没有关系的注释,防碍了代码的可读性。第二个不足是:虽然注释对于与某个组件相关的元数据来说很理想,但是对于跨组件应用程序的元数据,它并不是非常合适。

而在另一方面 ,对于应用程序中跨越不同组件的关系网络,配置文件可以提供一个有组织的视图。因为它们独立于实际的源代码,所以不会防碍 Java 源代码的可读性。配置文件的主要不足就是前面所提到的:它们是独立的工件,需要与应用程序的源代码并行维护,而在两者之间没有明显的联系。

注释误用

作为我认为的注释误用的一个示例,清单 1 给出了 JAX-RPC 2.0 的早期访问版本提供的注释示例的一段摘录(这段代码是在早期访问发行版使用的 Java Research License 的发行许可之下出现在这里的)。声明: 虽然我是 JAX-RPC 2.0 专家组的成员,但这里所表述的观点,仅属于我个人的意见,并不代表整个专家组。


清单 1. JAX-RPC 2.0 注释示例

            /*
            * Copyright (c) 2005 Sun Microsystems, Inc.
            * All rights reserved.
            */
            package annotations.server;
            import java.rmi.Remote;
            import java.rmi.RemoteException;
            import javax.jws.WebService;
            import javax.jws.WebMethod;
            import javax.jws.soap.SOAPBinding;
            import javax.jws.WebResult;
            import javax.jws.WebParam;
            @WebService(targetNamespace = "http://duke.org", name="AddNumbers")
            @SOAPBinding(style=SOAPBinding.Style.RPC, use=SOAPBinding.Use.LITERAL)
            public interface AddNumbersIF extends Remote {
            @WebMethod(operationName="add", soapAction="urn:addNumbers")
            @WebResult(name="return")
            public int addNumbers(
            @WebParam(name="num1")int number1,
            @WebParam(name="num2")int number2) throws RemoteException, AddNumbersException;
            }
            /*
            * Copyright (c) 2005 Sun Microsystems, Inc.
            * All rights reserved.
            */
            package annotations.server;
            import javax.jws.WebService;
            @WebService(endpointInterface="annotations.server.AddNumbersIF")
            public class AddNumbersImpl {
            /**
            * @param number1
            * @param number2
            * @return The sum
            * @throws AddNumbersException
            *             if any of the numbers to be added is negative.
            */
            public int addNumbers(int number1, int number2) throws AddNumbersException {
            if (number1 < 0 || number2 < 0) {
            throw new AddNumbersException("Negative number can't be added!",
            "Numbers: " + number1 + ", " + number2);
            }
            return number1 + number2;
            }
            }
            

清单 1 的代码包含一个 Web 服务接口定义和对应的实现类。虽然接口定义是用 Java 代码表示的(或者是这样的代码 —— 我在最近的一次讲座中介绍这个示例时,有位听众指出,如果我不是像这样指出的话,他可能认不出这是 Java 代码),它实际上是用注释编码的配置文件。

清单 1 中使用的注释是否确实提供了比独立配置文件更多的好处呢?清单 2 给出了一个配置文件,提供了与清单 1 相同的信息(使用 Apache Axis 的内部配置格式)。对我来说,配置文件的形式看起来更整齐一些,因此也就比示例中显示的一堆注释更容易维护。如果确实想把实现类作为 Web 服务绑定到它的使用上(清单 1 实现类中的 @WebService 注释的一些明显之处),那么可以仍然将注释只用作此目的(引用配置文件,而不是引用接口)。否则,根本不需要在实现类中包含任何特殊信息。


清单 2. 与清单 1 的注释等价的配置文件

            <deployment xmlns="http://xml.apache.org/axis/wsdd/"
            xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
            <service name="AddNumbers" provider="java:RPC" style="rpc" use="literal">
            <namespace>http://duke.org</namespace>
            <parameter name="className" value="annotations.server.AddNumbersImpl"/>
            <parameter name="allowedMethods" value="add"/>
            <operation name="add" qname="tns:addNumbers" returnQName="return">
            <parameter name="num1"/>
            <parameter name="num2"/>
            </operation>
            </service>
            </deployment>
            

注释的败笔之处

在最后一节,我给出一个示例,我认为它是注释使用不当的示例,在这个示例中,独立的配置文件可以工作得很好,并且更容易理解。作为配置文件比注释工作得好的示例,我将转用 JiBX 数据绑定框架 —— 这是我自己喜欢的类处理框架。

JiBX 使用字节码增强向编译后的类添加方法,以便在类的实例与 XML 之间进行转换。Java 类和 XML 之间的关系由绑定定义决定,绑定定义是一种 XML 配置文件的工。这个配置由 JiBX 框架的绑定编译器组件处理,它实现了编译后的类的实际字节码增强。

JiBX 与其他数据绑定框架的不同之处(不谈速度 —— 它一般要比其他框架快几倍)是 Java 类与 XML 之间关系的灵活性。大多数数据绑定框架假设 Java 类的结构与 XML 的结构进行匹配,这样,包含属性或子元素的元素就与类似 JavaBean 的类对应,而单一属性或单一子元素则对应着类似 JavaBean 的类的简单属性。JiBX 消除了这个假设,采用的绑定定义支持 Java 类与 XML 表示之间的结构性差异。JiBX 甚至还支持对相同的类进行多重绑定,实际的绑定将用于运行时选定的某个特殊 XML 文档。

为了说明问题,我将提供一个 JiBX 灵活性的简单示例。清单 3 显示了一对数据类,清单 4 和清单 5 则分别给出了这两个类的绑定定义和对应的 XML 文档。两个示例 XML 文档表示的都是相同的数据,多数开发人员会发现,虽然这两个文档的结构明显不同,但很容易将这两个文档与清单 3 的 Java 类联系在一起。多数开发人员还会发现,可以很容易地理解对应的绑定定义是如何与每个 XML 文档的结构关联的。


清单 3. XML 绑定的数据类

            package simple;
            public class Customer
            {
            private Name name;
            private String street1;
            private String street2;
            private String city;
            private String state;
            private String zip;
            private String phone;
            ...
            }
            package simple;
            public class Name
            {
            private String firstName;
            private String lastName;
            ...
            }
            


清单 4. 第一个绑定和 XML 文档

            <binding name="binding1">
            <mapping name="customer" class="simple.Customer">
            <structure field="name">
            <value name="first-name" field="firstName"/>
            <value name="last-name" field="lastName"/>
            </structure>
            <value name="street" field="street1"/>
            <value name="city" field="city"/>
            <value name="state" field="state"/>
            <value name="zip" field="zip"/>
            <value name="phone" field="phone"/>
            </mapping>
            </binding>
            <customer>
            <first-name>John</first-name>
            <last-name>Smith</last-name>
            <street>12345 Happy Lane</street>
            <city>Plunk</city>
            <state>WA</state>
            <zip>98059</zip>
            <phone>888.555.1234</phone>
            </customer>
            


清单 5. 第二个绑定和 XML 文档

            <binding name="binding2">
            <mapping name="customer" class="simple.Customer">
            <structure name="name" field="name">
            <value name="first-name" field="firstName"/>
            <value name="last-name" field="lastName"/>
            </structure>
            <structure name="address">
            <value name="street" field="street1"/>
            <value name="city" field="city"/>
            <value style="attribute" name="state" field="state"/>
            <value style="attribute" name="zip" field="zip"/>
            </structure>
            <value style="attribute" name="phone" field="phone"/>
            </mapping>
            </binding>
            <customer phone="888.555.1234">
            <name>
            <first-name>John</first-name>
            <last-name>Smith</last-name>
            <name>
            <address state="WA" zip="98059">
            <street>12345 Happy Lane</street>
            <city>Plunk</city>
            </address>
            </customer>
            

现在考虑一下,如果使用注释来定义清单 4 和清单 5 的绑定,将涉及哪些内容。清单 6 给出了这类注释的一个版本。在这里,我假设了一些表示能在绑定定义中使用的不同选项的注释,其中大多数注释都有进行单一绑定(使用简单值)和多重绑定(使用一组值)的变体。即使对于这个简单示例,结果也非常混乱,至少对我来说,阅读这样的 Java 代码(比起未加注释的版本)或者了解绑定定义(比起清单 4 和清单 5 的定义)似乎更难一些。


清单 6. 为了绑定而进行注释的 Java 类

            package simple;
            // Need the JiBXAddedWrappers annotation here because there's no other place to
            //  put it. The ugly format of the annotation is necessary because you can't
            //  repeat an annotation, and can only have array values of base types.
            @JiBXMapping(names={"binding1", "binding2"})
            @JiBXAddedWrappers(bindings={"binding2"}, names={"address"}, field-sets={"street1,city.state,zip"})
            public class Customer
            {
            @JiBXStructure(bindings={"binding1", "binding2"}, names={"", "name"}) private Name name;
            @JiBXValue(name="street") private String street1;
            private String street2;
            @JiBXValue private String city;
            @JiBXValue(bindings={"binding1", "binding2"}, styles={element, attribute})
            private String state;
            @JiBXValue(bindings={"binding1", "binding2"}, styles={element, attribute})
            private String zip;
            @JiBXValue(bindings={"binding1", "binding2"}, styles={element, attribute})
            private String phone;
            ...
            }
            package simple;
            public class Name
            {
            @JiBXValue(name="first-name") private String firstName;
            @JiBXValue(name="last-name") private String lastName;
            ...
            }
            

在我开始开发 JiBX 时,我最初计划提供 XDoclet 支持,为那些不想维护独立绑定文件的用户提供一个方便的备选解决方案。但是,因为试图表现 JiBX 绑定的全面灵活性,这很快使事物变得一团糟。XDoclet 标签承受了与注释相同的限制,所以 XDoclet 版本的绑定看起来与清单 6 的注释版本很相似。所以我决定最好还是强制使用绑定定义文件,而不是怂恿用户陷入用标签定义绑定元数据的歧路。

最佳实践

在这一点上,我希望已经说服了您:注释并不总是比配置文件好。这就留下了一个问题,也就是说,要判断在具体的使用情况下,哪种方式工作得更好。对我来说,看起来答案与需要表示的配置信息类型有关。

对于总要与特定 Java 组件(类、方法或字段)连接的配置信息,用注释表示是一个好的选择。当配置是代码的核心目标时,注释工作得特别好。因为注释方面的限制,所以当每个组件只有一个配置时,注释也是最适合的。如果需要处理多重配置,特别是配置取决于包含注释的 Java 类之外的东西时,注释带来的问题可能要比它们解决的问题还要多。最后,如果不重新编译 Java 源代码,就不能修改注释,所以要求在运行时配置的内容不能使用注释。

早期专栏 中,我提供了一个使用注释自动构建 toString() 方法的示例。虽然这只是一个微不足道的示例,但是我认为它确实表现了注释的正确用法:注释适用于某个直接涉及代码的目标(toString() 方法),不要求多重配置(因为每个类永远只能有一个方法实例),只在类单元内部工作,并且不需要在运行时进行改变。

而另一方面,当以协作的方式使用不同的 Java 组件时,配置文件工作得最好。最后一节的 JiBX 绑定定义就是这个事实的一个最好例子:用注释将配置嵌入代码中会使代理和配置都难以被人们所理解。我将数据绑定函数看作是将要应用到程序程序的方面(从 AOP 的角度),这甚至超出了这个混淆问题的范围。AOP 的立场来看,最好是把方面信息(在该例是绑定定义)放在代码之外。





结束语

在这期专栏中,我介绍了我认为是用基于注释的方法表示配置信息的弱点的地方。除了这些弱点,我认为注释是对 Java 开发人员工具包非常有用的一个补充。另一方面,我还认为它会被会滥用,而目前正在开发的一些 Java 标准可能正朝着这个方向发展。

对于我自己的 JiBX 项目,我倾向于依然采用配置文件,因为我认为这些对表示 JiBX 绑定更合适一些。即使如此,我对在 JiBX 2.0 中添加对 JAXB 2.0 注释的支持也很感兴趣(希望直接采用注释,并把它们转换成 JiBX 绑定定义文件)。通过这种方法,真想采用注释的用户可以使用标准的表示方式,而想得到 JiBX 绑定的全面灵活性的用户则可以转而采用绑定定义文件。

下个月,我将略微深入地研究 JiBX。在撰写这一期的文章时,我已经完成 JiBX 1.0 的生产发行版本,并计划立刻开始着手对 2.0 版本进行有计划的改进。对于我从 JiBX 1.0 的开发过程中学到的关于类处理是什么和不是什么的知识,现在是做一总结的好时候,这就是我在下个月的专栏中将要做的工作。请回过头来看看在 JiBX 字节码生成内核幕后所做的工作。

原文转自:http://www.ltesting.net

评论列表(网友评论仅供网友表达个人看法,并不表明本站同意其观点或证实其描述)