下面进一步深入分析Java注解的机制。
/** * Describes the Request-For-Enhancement(RFE) that led * to the presence of the annotated API element. */ public @interface RequestForEnhancement { int id(); String synopsis(); String engineer() default "[unassigned]"; String date(); default "[unimplemented]"; }
代码中只定义了一个annotation类型RequestForEnhancement。
2、修饰方法的annotation声明方式:
annotation是一种修饰符,能够如其它修饰符(如public、static、final)一般使用。习惯用法是annotaions用在其它的修饰符前面。annotations由“@+annotation类型+带有括号的成员-值列表”组成。这些成员的值必须是编译时常量(即在运行时不变)。
A:下面是一个使用了RequestForEnhancement annotation的方法声明:
清单2:
@RequestForEnhancement( id = 2868724, synopsis = "Enable time-travel", engineer = "Mr. Peabody", date = "4/1/3007" ) public static void travelThroughTime(Date destination) { ... }B:当声明一个没有成员的annotation类型声明时,可使用以下方式:
/** * Indicates that the specification of the annotated API element * is preliminary and subject to change. */ public @interface Preliminary { }作为上面没有成员的annotation类型声明的简写方式:
@Preliminary public class TimeTravel { ... }C: 如果在annotations中只有唯一一个成员,则该成员应命名为value:
/** * Associates a copyright notice with the annotated API element. */ public @interface Copyright { String value(); }更为方便的是对于具有唯一成员且成员名为value的annotation(如上文),在其使用时可以忽略掉成员名和赋值号(=):
@Copyright("2002 Yoyodyne Propulsion Systems") public class OscillationOverthruster { ... }D:下面是一个复杂的Annotataion类型声明,其成员是Annotation类型的数组
import java.lang.annotation.*; /** * Reviews annotation类型只有一个成员, * 由Review annotation组成的数组 */ @Retention(RetentionPolicy.RUNTIME) public @interface Reviews { Review[] value(); } /** * Review annotation类型有3个成员: * 枚举类型成员grade * 表示Review名称的字符串类型成员Reviewer * 具有默认值的字符串类型成员Comment。 */ public @interface Review { // 内嵌的枚举类型 public static enum Grade { EXCELLENT, SATISFACTORY, UNSATISFACTORY }; // 下面的方法定义了annotation的成员 Grade grade(); String reviewer(); String comment() default ""; }Reviews annotation类型只有一个成员,但是这个成员的类型是复杂的:由Review annotation组成的数组。Review annotation类型有3个成员:枚举类型成员grade、表示Review名称的字符串类型成员Reviewer、具有默认值的字符串类型成员Comment。
最后,我们来定义一个annotation方法用于罗列出类运行中所有的unchecked异常(这种情况不一定是错误)。这个annotation类型将一个数组作为了唯一的成员。数组中的每个元素都是异常类。为了加强对未检查的异常(此类异常都是在运行时抛出)进行报告,我们可以在代码中对异常的类型进行限制:
public @interface UncheckedExceptions { Class<? extends RuntimeException>[] value(); }3、一个使用实例:
import java.lang.annotation.*; /** * Indicates that the annotated method is a test method. * This annotation should be used only on parameterless static methods. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface Test { }值得注意的是annotaion类型声明是可以标注自己的,这样的annotation被称为“meta-annotations”。
public class Foo { @Test public static void m1() { } public static void m2() { } @Test public static void m3() { throw new RuntimeException("Boom"); } public static void m4() { } @Test public static void m5() { } public static void m6() { } @Test public static void m7() { throw new RuntimeException("Crash"); } public static void m8() { } }使用测试:
import java.lang.reflect.*; public class RunTests { public static void main(String[] args) throws Exception { int passed = 0, failed = 0; for (Method m : Class.forName(args[0]).getMethods()) { if (m.isAnnotationPresent(Test.class)) { try { m.invoke(null); passed++; } catch (Throwable ex) { System.out.printf("Test %s failed: %s %n", m, ex.getCause()); failed++; } } } System.out.printf("Passed: %d, Failed %d%n", passed, failed); } }这个程序从命令行参数中取出类名,并且遍历此类的所有方法,尝试调用其中被上面的测试annotation类型标注过的方法。在此过程中为了找出哪些方法被annotation类型标注过,需要使用反射的方式执行此查询。如果在调用方法时抛出异常,此方法被认为已经失败,并打印一个失败报告。最后,打印运行通过/失败的方法数量。
$ java RunTests Foo Test public static void Foo.m3() failed: java.lang.RuntimeException: Boom Test public static void Foo.m7() failed: java.lang.RuntimeException: Crash Passed: 2, Failed 2
注解在很多框架、类库中有广泛的应用。如JUnit测试框架, Spring, Hibernate, EJB等等。这也是开发人员所常常用到的一种方式。
上面的介绍说明了annotation的使用方法、定义方式、分类。初学者可以通过以上的说明制作简单的annotation程序,但是对于一些高级的annotation应用(例如使用自定义annotation生成javabean映射xml文件)还需要进一步的研究和探讨。同时,annotation运行存在两种方式:运行时、编译时。上文中讨论的都是在运行时的annotation应用,但在编译时的annotation应用还没有涉及,因为编译时的annotation要使用annotation processing tool(APT)。
annotation本身使用时十分简便。例如一个本地变量可以被一个以NonNull命名的annotation类型所标注,来作为对这个本地变量不能被赋予null值的断言。而我们可以编写与之配套的一个annotation代码分析工具,使用它来对具有前面变量的代码进行解析,并且尝试验证这个断言。当然这些代码并不必自己编写。在JDK安装后,在JDK/bin目录中可以找到名为“apt”的工具,它提供了处理annotation的框架:它启动后扫描源代码中的annotation,并调用我们定义好的annotation处理器完成我们所要完成的工作(比如验证前面例子中的断言)。说到这里,annotation的强大功能似乎可以替代XDoclet这类的工具了,随着我们的深入,大家会更加坚信这一点。
注:详细描述请参看JSR 250规范 http://www.jcp.org/aboutJava/communityprocess/pfd/jsr250/
四、annotation实例分析
1.BRFW(Beaninfo Runtime FrameWork)定义:
本人编写的一个annotation功能演示框架。顾名思义,BRFW就是在运行时取得bean信息的框架。
2.BRFW的功能:
A.源代码级annotation:在bean的源代码中使用annotation定义bean的信息;
B.运行时获取bean数据:在运行时分析bean class中的annotation,并将当前bean class中field信息取出,功能类似xdoclet;
C.运行时bean数据的xml绑定:将获得的bean数据构造为xml文件格式展现。熟悉j2ee的朋友知道,这个功能类似JAXP。
3.BRFW框架:
BRFW主要包含以下几个类:
A.Persistent类:定义了用于修饰类的固有成员变量的annotation。
B.Exportable类:定义了用于修饰Class的类型的annotation。
C.ExportToXml类:核心类,用于完成BRFW的主要功能:将具有Exportable Annotation的bean对象转换为xml格式文本。
D.AddressForTest类:被A和B修饰过的用于测试目的的地址bean类。其中包含了地址定义所必需的信息:国家、省级、城市、街道、门牌等。
E.ContactBookForTest类:被A和B修饰过的友人通讯录bean类。其中包含了通讯录所必备的信息:友人姓名、年龄、电话、住址(成员为AddressForTest类型的ArrayList)、备注。需要说明的是电话这个bean成员变量是由字符串类型组成的ArrayList类型。由于朋友的住址可能不唯一,故这里的住址为由AddressForTest类型组成的ArrayList。
从上面的列表中,可以发现A、B用于修饰bean类和其类成员;C主要用于取出bean类的数据并将其作xml绑定,代码中使用了E作为测试类;E中可能包含着多个D。
在了解了这个简单框架后,我们来看一下BRFW的代码吧!
4.BRFW源代码分析:
A.Persistent类:
package beaninfoframework; import java.lang.annotation.*; /** * 用于修饰类的成员变量的annotation * @author jackzhou */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Persistent { String value() default ""; }B.Exportable类:
package beaninfoframework; import java.lang.annotation.*; /** * 用于修饰类类型的annotation * @author jackzhou */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Exportable { // 名称 String name() default ""; // 描述 String description() default ""; // 省略name和description后,用来保存name值 String value() default ""; }C.AddressForTest类:
package beaninfoframework; /** * 用于测试的地址类 * @author jackzhou */ @Exportable("address") public class AddressForTest { //国家 @Persistent private String country = null; //省级 @Persistent private String province = null; //城市 @Persistent private String city = null; //街道 @Persistent private String street = null; //门牌 @Persistent private String doorplate = null; public AddressForTest(String country, String province, String city, String street, String doorplate) { this.country = country; this.province = province; this.city = city; this.street = street; this.doorplate = doorplate; } }D.ContactBookForTest类:
package beaninfoframework; import java.util.ArrayList; /** * 用于测试的友人通讯录类 * 包含:姓名、年龄、电话、住址(多个)、备注 * @author jackzhou */ @Exportable(name = "contactbook", description = "contact book") public class ContactBookForTest { //友人姓名 @Persistent private String friendName = null; //友人年龄 @Persistent private int age = 0; //友人电话 @Persistent private ArrayList<String> telephones = null; //友人住址:家庭、单位 @Persistent private ArrayList<AddressForTest> AddressForText = null; //备注 @Persistent private String note = null; public ContactBookForTest(String name, int age, ArrayList<String> telephoneList, ArrayList<AddressForTest> addressList, String note) { this.friendName = name; this.age = age; this.telephones = new ArrayList<>(telephoneList); this.AddressForText = new ArrayList<>(addressList); this.note = note; } }E.ExportToXml类:
package beaninfoframework; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.Map; /** * 将具有Exportable Annotation的对象转换为xml格式文本 * @author jackzhou */ public class ExportToXml { /** * 返回对象的成员变量的值(字符串类型) * @param field 对象的成员变量 * @param fieldTypeClass 对象的类型 * @param obj 对象 * @return 对象的成员变量的值(字符串类型) */ private String getFieldValue(Field field, Class fieldTypeClass, Object obj) { String value = null; try { if (fieldTypeClass == String.class) { value = (String) field.get(obj); } else if (fieldTypeClass == int.class) { value = Integer.toString(field.getInt(obj)); } else if (fieldTypeClass == long.class) { value = Long.toString(field.getLong(obj)); } else if (fieldTypeClass == short.class) { value = Short.toString(field.getShort(obj)); } else if (fieldTypeClass == float.class) { value = Float.toString(field.getFloat(obj)); } else if (fieldTypeClass == double.class) { value = Double.toString(field.getDouble(obj)); } else if (fieldTypeClass == byte.class) { value = Byte.toString(field.getByte(obj)); } else if (fieldTypeClass == char.class) { value = Character.toString(field.getChar(obj)); } else if (fieldTypeClass == boolean.class) { value = Boolean.toString(field.getBoolean(obj)); } } catch (Exception ex) { ex.printStackTrace(); value = null; } return value; } /** * 输出对象的字段,当对象的字段为Collection或者Map类型时, * 要调用exportObject方法继续处理 * @param obj 被处理的对象 * @throws Exception */ public void exportFields(Object obj) throws Exception { Exportable exportable = obj.getClass().getAnnotation(Exportable.class); if (exportable != null) { if (exportable.value().length() > 0) { //System.out.println("Class annotation Name:"+exportable.value()); } else { //System.out.println("Class annotation Name:"+exportable.name()); } } else { //System.out.println(obj.getClass()+"类不是使用Exportable标注过的"); } //取出对象的成员变量 Field[] fields = obj.getClass().getDeclaredFields(); for (Field field : fields) { //获得成员变量的标注 Persistent fieldAnnotation = field.getAnnotation(Persistent.class); if (fieldAnnotation == null) { continue; } //重要:避免java虚拟机检查对私有成员的访问权限 field.setAccessible(true); Class typeClass = field.getType(); String name = field.getName(); String value = getFieldValue(field, typeClass, obj); //如果获得成员变量的值,则输出 if (value != null) { System.out.println(getIndent() + "<" + name + ">\n" + getIndent() + "\t" + value + "\n" + getIndent() + "</" + name + ">"); } //处理成员变量中类型为Collection或Map else if ((field.get(obj) instanceof Collection) || (field.get(obj) instanceof Map)) { exportObject(field.get(obj)); } else { exportObject(field.get(obj)); } } } //缩进深度 int levelDepth = 0; //防止循环引用的检查者,循环引用现象如:a包含b,而b又包含a Collection<Object> cyclicChecker = new ArrayList<>(); /** * 返回缩进字符串 * @return */ private String getIndent() { String s = ""; for (int i = 0; i < levelDepth; i++) { s += "\t"; } return s; } /** * 输出对象,如果对象类型为Collection和Map类型, * 则需要递归调用exportObject进行处理 * @param obj * @throws Exception */ public void exportObject(Object obj) throws Exception { Exportable exportable = null; String elementName = null; //循环引用现象处理 if (cyclicChecker.contains(obj)) { return; } cyclicChecker.add(obj); //首先处理Collection和Map类型 if (obj instanceof Collection) { for (Iterator i = ((Collection) obj).iterator(); i.hasNext();) { exportObject(i.next()); } } else if (obj instanceof Map) { for (Iterator i = ((Map) obj).keySet().iterator(); i.hasNext();) { exportObject(i.next()); } } else { exportable = obj.getClass().getAnnotation(Exportable.class); //如果obj已经被Exportable Annotation修饰过了(注意annotation是具有继承性的), //则使用其name作为输出xml的元素name if (exportable != null) { if (exportable.value().length() > 0) { elementName = exportable.value(); } else { elementName = exportable.name(); } } //未被修饰或者Exportable Annotation的值为空字符串, //则使用类名作为输出xml的元素name if (exportable == null || elementName.length() == 0) { elementName = obj.getClass().getSimpleName(); } //输出xml元素头 System.out.println(getIndent() + "<" + elementName + ">"); levelDepth++; //如果没有被修饰,则直接输出其toString()作为元素值 if (exportable == null) { System.out.println(getIndent() + obj.toString()); } else { //否则将对象的成员变量导出为xml exportFields(obj); } levelDepth--; //输出xml元素结尾 System.out.println(getIndent() + "</" + elementName + ">"); } cyclicChecker.remove(obj); } public static void main(String[] argv) { try { AddressForTest ad = new AddressForTest("China", "Beijing", "Beijing", "winnerStreet", "10"); ExportToXml test = new ExportToXml(); ArrayList<String> telephoneList = new ArrayList<>(); telephoneList.add("66608888"); telephoneList.add("66608889"); ArrayList<AddressForTest> adList = new ArrayList<>(); adList.add(ad); ContactBookForTest adl = new ContactBookForTest("coolBoy", 18, telephoneList, adList, "some words"); test.exportObject(adl); } catch (Exception ex) { ex.printStackTrace(); } } }在ExportToXml类之前的类比较简单,这里必须说明一下ExportToXml类: 此类的核心函数是exportObject和exportFields方法,前者输出对象的xml信息,后者输出对象成员变量的信息。由于对象类型和成员类型的多样性,所以采取了以下的逻辑:
<contactbook> <friendName> coolBoy </friendName> <age> 18 </age> <String> 66608888 </String> <String> 66608889 </String> <address> <country> China </country> <province> Beijing </province> <city> Beijing </city> <street> winnerStreet </street> <doorplate> 10 </doorplate> </address> <note> some words </note> </contactbook>
1.何谓APT?
根据sun官方的解释,APT(annotation processing tool)是一个命令行工具,它对源代码文件进行检测找出其中的annotation后,使用annotation processors来处理annotation。而annotation processors使用了一套反射API并具备对JSR175规范的支持。
annotation processors处理annotation的基本过程如下:首先,APT运行annotation processors,根据提供的源文件中的annotation生成源代码文件和其它的文件(文件具体内容由annotation processors的编写者决定),接着APT将生成的源代码文件和提供的源文件进行编译生成类文件。
简单的和前面所讲的annotation实例BRFW相比,APT就像一个在编译时处理annotation的javac。而且从sun开发者的blog中看到,java1.6 beta版中已将APT的功能写入到了javac中,这样只要执行带有特定参数的javac就能达到APT的功能。实际上,APT在Java 8中已经被移除,可参看http://openjdk.java.net/projects/jdk8/features,其中的JEP 117。它的功能已经完全集成到javac编译器中去了。
2.为何使用APT?
使用APT主要目的是简化开发者的工作量,因为APT可以在编译程序源代码的同时,生成一些附属文件(比如源文件、类文件、程序发布描述文字等),这些附属文件的内容也都是与源代码相关的。换句话说,使用APT就是代替了传统的对代码信息和附属文件的维护工作。使用过hibernate或者beehive等软件的朋友可能深有体会。APT可以在编译生成代码类的同时将相关的文件写好,比如在使用beehive时,在代码中使用annotation声明了许多struct要用到的配置信息,而在编译后,这些信息会被APT以struct配置文件的方式存放。
3.如何定义processor?
A.APT工作过程:
从整个过程来讲,首先APT检测在源代码文件中哪些annotation存在。然后APT将查找我们编写的annotation processor factories类,并且要求factories类提供annotation processor以处理源文件中所涉及的annotation。接下来,一个合适的annotation processors将被执行,如果在processors生成源代码文件时,该文件中含有annotation,则APT将重复上面的过程直到没有新文件生成。
B.编写annotation processors:
编写一个annotation processors需要使用java1.5 lib目录中的tools.jar提供的以下4个包:
com.sun.mirror.apt: 和APT交互的接口;
com.sun.mirror.declaration: 用于模式化类成员、类方法、类声明的接口;
com.sun.mirror.type: 用于模式化源代码中类型的接口;
com.sun.mirror.util: 提供了用于处理类型和声明的一些工具。
每个processor实现了在com.sun.mirror.apt包中的AnnotationProcessor接口,这个接口有一个名为“process”的方法,该方法是在APT调用processor时将被用到的。一个processor可以处理一种或者多种annotation类型。
一个processor实例被其相应的工厂返回,此工厂为AnnotationProcessorFactory接口的实现。APT将调用工厂类的getProcessorFor方法来获得processor。在调用过程中,APT将提供给工厂类一个AnnotationProcessorEnvironment 类型的processor环境类对象,在这个环境对象中,processor将找到其执行所需要的每件东西,包括对所操作的程序结构的参考,与APT通讯并合作一同完成新文件的建立和警告/错误信息的传输。
提供工厂类有两个方式:通过APT的“-factory”命令行参数提供,或者让工厂类在APT的发现过程中被自动定位(关于发现过程详细介绍请看http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html)。前者对于一个已知的factory来讲是一种主动而又简单的方式;而后者则是需要在jar文件的META-INF/services目录中提供一个特定的发现路径:
在包含factory类的jar文件中作以下的操作:在META-INF/services目录中建立一个名为com.sun.mirror.apt.AnnotationProcessorFactory 的UTF-8编码文件,在文件中写入所有要使用到的factory类全名,每个类为一个单独行。
在JDK 8中,tools.jar中的这4个包已经被移除了,因此我们不再举apt的实际例子来分析了。到此为止 :)
参考文献:
http://www.blogjava.net/ericwang/archive/2005/12/13/23743.html
http://www.blogjava.net/weidagang2046/articles/28321.html
http://www.blogjava.net/weidagang2046/articles/27958.html