读Java编程思想随笔の注解
注解(也被称为元数据)为我们在代码中添加信息提供了一种形式化方法,是我们可以在稍后某个时刻方便地使用这些数据。
注解,在一定程度上把元数据和源代码文件结合在一起,而不是保存在外部文档中(大势所趋)。
注解是众多引入到Java SE5的重要的语言变化之一。它们可以提供用来完整地描述程序所需的信息,而这些信息是无法用Java来表达的。因此,注解使得我们能够以将由编译器来测试和验证的格式,存储程序有关的额外信息。注解可以用来生成描述符文件,甚至或是新的类定义,并且有助于减轻编写样板代码的负担。通过使用注解,我们可以将这些元数据保存在Java源代码中,并利用annotation API为自己的注解构造处理工具,同时,注解的优点还包括:更加干净易读的代码以及编译期类型检查等。
注解的语法比较简单,除了@符号使用之外,它基本与Java固有的语法一致。Java SE5内置了三种,定义在java.lang中的注解:
@Override,表示当前的方法定义将覆盖超类中的方法。如果你不小心拼写错误,或者方法签名对不上被覆盖的方法,编译器就会发出错误提示。
@Deprecated,如果程序员使用了注解为它的元素,那么编译器就会发出警告信息。
@SuppressWarnings,关闭不当的编译器警告信息。在Java SE5之前的版本中,也可以使用该注解,不过会被忽略不起作用。
每当你创建描述符性质的类或接口时,一旦其中包含了重复性工作,那就可以考虑使用注解来简化和自动化该过程。
注解的出现,可以替换某些现存的系统。例如XDoclet,它是一个独立的文档化工具,专门设计用来生成类似注解一样的文档。与之相比,注解是真正的语言级的概念,一旦构造出来,就享有编译期的类型检查保护。注解是在实际的源代码级别保存所有的信息,而不是某种注释性的文字,这使得代码更整洁,且便于维护。通过使用扩展的annotation API,或外部的字节码工具类库,程序员拥有对源代码以及字节码强大而检查与操作能力。
基本语法
在下面的例子中,使用@Test对testExecute()方法进行注解。该注解本身不做任何事情,但是编译器要确保在其构造路径上必须有@Test注解的定义。
1 public class Testable { 2 public void execute(){ 3 System.out.println("Executing..."); 4 } 5 @Test 6 void testExecute(){ 7 execute(); 8 } 9 }
被注解的方法与其他方法没有区别。在这个例子中,注解@Test可以与任何修饰符共同作用于方法,例如public、static或void。从语法角度看,注解的使用方法与修饰符的使用几乎是一模一样。
定义注解
下面是前例中用到的注解@Test的定义。可以看到,注解的定义看起来很像接口的定义。事实上,与Java其他任何接口一样,注解也将会编译成class文件。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Test { }
除了@符号以外,@Test的定义很像一个空的接口。定义注解时,会需要一些元注解(meta-annotation),如@Target和@Retention。@Target用来定义你的注解将应用于什么地方(例如是一个方法还是一个域)。@Retention用来定义该注解在哪一个级别可用(例如在源代码中(SOURCE)、类文件中(CLASS)或者运行时(RUNTIME))。
在注解中,一般都会包含一些元素以表示某些值。当分析处理注解时,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,唯一的区别是你可以为其指认默认值。
没有元素的注解称之为标记注解,例如@Test。
下面是一个简单的注解,我们可以利用它来跟踪一个项目中的用例。如果一个方法或一组方法实现了某个用例的需求,那么程序员可以为此方法加上注解。于是,项目经理通过计算已经实现的用例,就可以很好的掌控项目的进展。而如果要更新或修改系统的业务逻辑,则维护该项目的开发人员也可以很容易在代码中找到对应的用例。
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface UseCase { 4 public int id(); 5 public String description() default "no description"; 6 7 }
注意,id和description类似方法定义。由于编译器会对id进行类型检查,因此将用例文档的追踪数据库与源代码相关联是可靠的。description元素有一个default值,如果在注解某个方法时没有给出description值,则该注解的处理器就会使用此元素的默认值。
1 public class PasswordUtils { 2 @UseCase(id=47,description="Passwords must contain at least one numeric") 3 public boolean validatePassword(String password){ 4 return (password.matches("\\w*\\d\\w*")); 5 } 6 7 @UseCase(id=48) 8 public String encryptPassword(String password){ 9 return new StringBuilder(password).reverse().toString(); 10 } 11 12 @UseCase(id=49,description="New passwords cant equal previously used ones") 13 public boolean checkForNewPassword(List<String> prevPasswords,String password){ 14 return !prevPasswords.contains(password); 15 } 16 17 }
注解的元素在使用时表现为名-值对的形式,并需要置于@UseCase声明之后的括号内。在encryptPassword()方法的注解中,并没有给出description元素的值,因此,在UseCase的注解处理器分析处理这个类时会使用该元素的默认值。
元注解
Java目前内置了三种标准注解,以及四种元注解。元注解专职负责注解其他的注解。

大多数时候,程序员主要是定义自己的注解,并编写自己的处理器来处理它们。
编写注解处理器
如果没有用来读取注解的工具,那注解也不会比注释更好用。使用注解的过程中,很重要的一个部分就是创建与使用注解处理器。Java SE5扩展了反射机制的API,以帮助程序员构造这类工具。同时,它还提供了一个外部工具apt帮助程序员解析带有注解的Java源代码。
1 public class UseCaseTracker { 2 public static void trackUseCases(List<Integer> useCases,Class<?> cl){ 3 for (Method m:cl.getDeclaredMethods()){ 4 UseCase uc = m.getAnnotation(UseCase.class); 5 if (uc!=null){ 6 System.out.println("Found Use Case:"+uc.id()+" "+uc.description()); 7 useCases.remove(new Integer(uc.id())); 8 } 9 } 10 for (int i: useCases){ 11 System.out.println("Warning:Missing use case-"+i); 12 } 13 } 14 15 public static void main(String[] args) { 16 List<Integer> useCases = new ArrayList<Integer>(); 17 Collections.addAll(useCases,47,48,49,50); 18 trackUseCases(useCases,PasswordUtils.class); 19 } 20 } 21 // Found Use Case:47 Passwords must contain at least one numeric 22 // Found Use Case:48 no description 23 // Found Use Case:49 New passwords cant equal previously used ones 24 // Warning:Missing use case-50
这个程序用到了两个反射的方法:getDeclaredMethods()和getAnnotation(),它们都属于AnnotatedElement接口。getAnnotation()方法返回指定类型的注解对象,在这里就是UseCase。如果被注解的方法上没有该类型的注解,则返回null值。
注解元素
标签@UseCase由UseCase.java定义,其中包含int元素id,以及一个String元素description。注解元素可用的类型如下所示:
所有基本类型
String
Class
enum
Annotation
以上类型的数组
如果你使用了其他类型,那编译器会报错。注意,也不允许使用任何包装类型,不过由于自动打包存在,这算不是什么限制。注解也可以作为元素的类型,也就是说注解可以嵌套。
默认值限制
编译器对元素的默认值有些过分挑剔。首先,元素不能有不确定的值。也就是说,元素必须要么具有默认值,要么在使用注解时提供元素的值。
其次,对于非基本类型的元素,无论是在源代码中声明时,或是在注解接口中定义默认值时,都不能以null作为其值。这个约束使得处理器很难表现一个元素的存在或缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且都具有相应的值。为了绕开这个约束,我们只能自己定义一些特殊的值,例如空字符串或负数,以此表示某个元素不存在。
1 @Target(ElementType.METHOD) 2 @Retention(RetentionPolicy.RUNTIME) 3 public @interface SimulatingNull { 4 public int id() default -1; 5 public String description() default ""; 6 }
生成外部文件
有些framework需要一些额外的信息才能与你的源代码协同工作,而这种情况最适合注解表现其价值了。像Enterprise Java Bean这样的技术,每一个Bean都需要大量的接口与部署来描述文件,而这些都属于样板文件。Web Service、自定义标签库以及对象关系映射工具等,一般都需要XML描述文件,而这些描述文件脱离于源代码之外。因此,在定义了Java类之后,程序员还必须得忍受着沉闷,重复地提供某些信息,例如类名和包名等已经在原始的类文件中提供了的信息。每当程序员使用外部的描述文件时,他就拥有了同一个类的两个单独的信息源,这经常导致代码同步问题。同时,它也要求为项目工作的程序员,必须同时知道如何编写Java类,以及如何编辑描述文件。
假设你希望提供一些基本的对象关系映射供呢个,能够自动生成数据库表,用以存储JavaBean对象。你可以选择使用XML描述文件,指明类的名字、每个成员以及数据库映射的相关信息。然而,使用注解的话,你可以将所有信息都保存在JavaBean源文件中。为此,我们需要一些新的注解,用以定义与Bean关联的数据库表的名字,以及与Bean属性关联的列的名字和SQL类型。
以下是一个注解的定义,它告诉注解处理器,你需要为我生成一个数据库表:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DBTable { public String name() default ""; }
在@Target(ElementType.TYPE)注解中指定的每一个ElementType就是一种约束,它告诉编译器,这个自定义的注解只能应用于该类型。程序员可以只指定enum ElementType中的某一个值,或者以逗号分隔的形式指定多个值。如果想将注解应用于所有的ElementType,那么可以省去@Target元注解。
注意,@DBTable有一个name()元素,该注解通过这个元素为处理器创建数据库表提供表的名字。
接下来是为修饰JavaBean域准备的注解:
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Constraints { boolean primaryKey() default false; boolean allowNull() default true; boolean unique() default false; }
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SQLInteger { String name() default ""; Constraints constraints() default @Constraints; }
@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SQLString { int value() default 0; String name() default ""; Constraints constraints() default @Constraints; }
注解处理器通过@Constraints注解取出数据库表的元数据。虽然对于数据库所能提供的所有约束而言,@Constraints注解只表示了它的一个很小的子集,不过它所要表达的思想已经很清楚了。
另外两个@interface定义的是SQL类型。如果希望这个framework更有价值的话,我们就应该为每种SQL类型都定义相应的注解。
这些SQL类型具有name()和constraints()元素。后者利用了嵌套注解的功能,将column类型的数据库约束星系嵌入其中。其中constraints()元素的默认值是@Constraints。由于在@Constraints注解类型之后,没有在括号中指明@Constraints中元素的值,因此,constraints()元素默认值实际上就是一个所有元素都为默认值的@Constraints注解。如果要令嵌入的@Constraints注解中的unique()元素为true,并以此作为constraints()元素的默认值,则需要如下定义该元素:
public @interface Uniqueness { Constraints constraints() default @Constraints(unique = true); }
下面是一个简单的Bean定义,我们在其中应用了以上这些注解
@DBTable(name="MEMBER") public class Member { @SQLString(30) String firstname; @SQLString(50) String lastname; @SQLInteger Integer age; @SQLString(value=30,constraints = @Constraints(primaryKey = true)) String handle; static int memberCount; @Override public String toString() { return handle; } public String getFirstname() { return firstname; } public String getLastname() { return lastname; } public Integer getAge() { return age; } public String getHandle() { return handle; } }
类的注解@DBTable给定了值MEMBER,它将会用来作为表的名字。Bean的属性firstname和lastname,都被注解为@SQLString类型,并且其元素分别为30和50.这些注解有两个有趣的地方:第一它们都使用了嵌入的@Constraints注解的默认值;第二,它们都使用了快捷方式。何谓快捷方式,如果程序员注解中定义了名为value的元素,并且在应用该注解时,如果该元素是唯一需要赋值的一个元素,那么此时无需使用名-值得这种语法,而只需在括号内给出value元素所需的值即可。这可以应用与任何合法类型的元素。当然了,这也限制了程序员必须将此元素命名为value。
实现处理器
实现处理器 拼接SQL,真正实现创建表的动作
public class TableCreator { public static void main(String[] args) throws Exception{ if (args.length<1){ System.out.println("arguments:annotated classes"); System.exit(0); } for (String className:args){ Class<?> cl = Class.forName(className); DBTable dbTable = cl.getAnnotation(DBTable.class); if (dbTable==null){ System.out.println("No DBTable annotation in class "+className); continue; } String tableName = dbTable.name(); if (tableName.length()<1){ tableName = cl.getName().toUpperCase(); } List<String> columnDefs = new ArrayList<String>(); for (Field field:cl.getDeclaredFields()){ String columnName = null; Annotation [] anns = field.getDeclaredAnnotations(); if (anns.length<1){ continue; } if (anns [0] instanceof SQLInteger){ SQLInteger sInt = (SQLInteger) anns[0]; if (sInt.name().length()<1){ columnName = field.getName().toUpperCase(); }else { columnName = sInt.name(); } columnDefs.add(columnName+" INT"+getConstraints(sInt.constraints())); } if (anns [0] instanceof SQLString){ SQLString sString = (SQLString) anns[0]; if (sString.name().length()<1){ columnName = field.getName().toUpperCase(); }else { columnName = sString.name(); } columnDefs.add(columnName+" VARCHAR("+sString.value()+")"+getConstraints(sString.constraints())); } StringBuilder createCommand = new StringBuilder("CREATE TABLE"+tableName+"("); for (String columnDef : columnDefs){ createCommand.append("\n "+columnDef+","); String tableCreate = createCommand.substring(0,createCommand.length()-1)+");"; System.out.println("Table Creation SQL for "+className+" is :\n"+tableCreate); } } } } private static String getConstraints(Constraints con){ String constraints = ""; if (!con.allowNull()){ constraints +=" NOT NULL"; } if (con.primaryKey()){ constraints +=" PRIMARY KEY"; } if (con.unique()){ constraints +=" UNIQUE"; } return constraints; } }
使用apt处理注解
注解处理工具apt,这是sun为了帮助注解的处理过程而提供的工具。由于这是该工具的第一版,其功能还比较基础,不过它确实有助于程序员的开发工作。
与javac一样,apt被设计操作Java源文件,而不是编译后的类。默认情况下,apt会处理完源文件后编译它们。如果在系统的构建过程中会自动创建一些新的源文件,那么这个特性非常有用。事实上,apt会检查新生成的源文件中的注解,然后将所有文件一同编译。
当注解处理器生成一个新的源文件时,该文件会在新一轮注解处理中接受检查。该工具会一轮一轮处理,直到不再有新的源文件产生为止。然后它再编译所有源文件。
程序员自定义的每一个注解都需要自己的处理器,而apt能够很容易地将多个注解处理器组合在一起。有了它,程序员就可以指定多个要处理的类,这比程序员遍历每个类文件要简单多了。此外还可以添加监听器,并在一轮注解处理结束的时候发送通知消息。
通过使用AnnotationProcessorFactory,apt能够为每一个它发现的注解生成一个正确的注解处理器。当你使用apt时,必须指明一个工厂类,或者指明能够找到apt工厂类的路径。否则,apt会踏上神秘的探索之旅。
使用apt生成注解处理器时,我们无法利用Java的反射机制,因为我们操作的是源文件而不是编译后的类。使用mirror api就能解决这个问题,它是我们可以在未经编译的源代码中查看方法、域以及类型。
下面是一个自定义的注解,使用它可以把一个类中的public方法提取出来,构造一个新的接口:
@Target(ElementType.TYPE) @Retention(RetentionPolicy.SOURCE) public @interface ExtractInterface { public String value(); }
RetentionPolicy是SOURCE,因为当我们从一个使用了该注解的类中抽取出接口之后,没有必要再保留这些注解信息。下面的类有一个公共方法,我们将会把它抽取到一个有用的接口中:
@ExtractInterface("IMultiplier")
public class Multiplier {
public int multiply(int x,int y){
int total=0;
for (int i=0;i<x;i++){
total = add (total,y);
}
return total;
}
private int add(int x,int y){
return x+y;
}
public static void main(String[] args) {
Multiplier m = new Multiplier();
System.out.println("11*16 = "+m.multiply(11,16));
}
}
在Multiplie类中,有一个 multiply()方法,该方法多次调用一个私有的方法add()以此实现相乘操作。add()方法不是公共的,因此不将其作为接口的一部分。注解给出了值IMultiplier,这就是将要生成的接口的名字:
/**
* @author yxm
* @date 2015/12/30 8:17
* @company 中国奇奇科技有限公司
* @description //通過MethodDeclaration以及getMethods()來找到public公共方法,不包括靜態方法。
* 一旦找到我們所需的public方法,就將其保存在一個ArrayList中,然後在一個.java文件中,創建新的接口中的方法定義。
*/
public class InterfaceExtractorProcessor implements AnnotationProcessor { private final AnnotationProcessorEnvironment env; private ArrayList<MethodDeclaration> interfaceMethods = new ArrayList<MethodDeclaration>();////通過MethodDeclaration以及getMethods()來找到public公共方法 /*處理器類的構造器以AnnotationProcessorEnvironment對象爲參數。 通過該對象我們就能知道apt正在處理的所有類型,並且可以通過它獲得Messager對象和Filter對象。 Messager對象可以用來向用戶報告信息,比如處理過程中發生的任何錯誤,以及錯誤在源代碼中出現的位置。 Filter是一種PrintWriter,我們可以通過它來創建新的文件。不使用普通PrintWriter而使用Filter對象的主要原因是, 只有這樣apt才能知道我們創建的新文件,從而對文件進行註解處理,並且在需要的時候編譯它們。 */ public InterfaceExtractorProcessor(AnnotationProcessorEnvironment env) { this.env = env; } /**所有的工作在這裏完成*/ public void process() { for(TypeDeclaration typeDecl : env.getSpecifiedTypeDeclarations()) { ExtractInterface annot = typeDecl.getAnnotation(ExtractInterface.class); if(annot == null) break; for(MethodDeclaration m : typeDecl.getMethods())//通過MethodDeclaration以及getMethods()來找到public公共方法 if(m.getModifiers().contains(Modifier.PUBLIC) && !(m.getModifiers().contains(Modifier.STATIC))) interfaceMethods.add(m); if(interfaceMethods.size() > 0) { try { /** * Filter createSourceFile()方法已將要新建的類或接口的名字,打開了一個普通的輸出流。 * 現在還沒有什麼工具幫助程序員創建Java語言結構,所以我們只能用普通的print()和println()來生成Java代碼。 * */ PrintWriter writer = env.getFiler().createSourceFile(annot.value()); writer.println("package " + typeDecl.getPackage().getQualifiedName() +";"); writer.println("public interface " + annot.value() + " {"); for(MethodDeclaration m : interfaceMethods) { writer.print(" public "); writer.print(m.getReturnType() + " "); writer.print(m.getSimpleName() + " ("); int i = 0; for(ParameterDeclaration parm : m.getParameters()) { writer.print(parm.getType() + " " + parm.getSimpleName()); if(++i < m.getParameters().size()) writer.print(", "); } writer.println(");"); } writer.println("}"); writer.close(); } catch(IOException ioe) { throw new RuntimeException(ioe); } } } } } ///:~
apt工具需要一个工厂类来为其指明正确的处理器,然后它才能调用处理器上的process()方法:
public class InterfaceExtractorProcessorFactory implements AnnotationProcessorFactory { /*** * AnnotationProcessorFactory接口只有三個方法,其中之一的getProcessorFor()方法返回註解處理器, * 此方法聲明Set<AnnotationTypeDeclaration> atds以及AnnotationProcessorEnvironment對象作爲參數。 * 另外兩個方法是supportedAnnotationTypes()和supportedOptions() ,程序員可以通過它們檢查一下,是否apt發現的所有註解都有 * 相應的處理器,是否所有控制檯輸入的參數都是你提供支持的選項。 * 其中supportedAnnotationTypes()方法尤其重要,因爲一旦在返回的String集合中沒有你的註解的完整類名, * apt就會抱怨沒有找到對應的處理器,從而發出警告消息,然後什麼也不做就退出。 */ public AnnotationProcessor getProcessorFor(Set<AnnotationTypeDeclaration> atds, AnnotationProcessorEnvironment env) { return new InterfaceExtractorProcessor(env); } public Collection<String> supportedAnnotationTypes() { return Collections.singleton("annotations.ExtractInterface"); } public Collection<String> supportedOptions() { return Collections.emptySet(); } } ///:~
通过处理器的println()语句,最终生成的IMultiplier.java会是这样的
public interface IMultiplier { public int multiply(int x,int y); }
基于注解的单元测试
单元测试是对类中每个方法提供一个或多个测试的实践,其目的是为了有规律地测试一个类的各个部分是否具备正确的行为。
public class AtUnitExample1 { public String methodOne(){ return "This is methodOne"; } public int methodTwo(){ System.out.println("This is methodTwo"); return 2; } @Test boolean methodOneTest(){ return methodOne().equals("This is methodOne"); } @Test public boolean m2(){ return methodTwo()==2; } @Test private boolean m3(){ return true; } @Test boolean failureTest(){ return false; } @Test boolean anotherDisappointment(){ return false; } public static void main(String[] args) throws Exception{ System.out.println("start"); OSExecute.command("java com.qiqi.annotations.AtUnit AtUnitExample1"); } } // annotations.AtUnitExample1 // . methodOneTest // . m2 This is methodTwo // // . m3 // . failureTest (failed) // . anotherDisappointment (failed) // (5 tests) // // >>> 2 FAILURES <<< // annotations.AtUnitExample1: failureTest // annotations.AtUnitExample1: anotherDisappointment // *///:~
程序运编写测试用例时,唯一需要做的就是决定测试是成功还是失败,应该返回true还是false。
程序员并非必须将测试方法嵌入到原本的类中,因为有时候这根本做不到。要生成一个非嵌入式的测试,最简单的方法就是继承。
public class AtUnitExternalTest extends AtUnitExample1 { @Test boolean _methodOne(){ return methodOne().equals("This is methodOne"); } @Test boolean _methodTwo(){ return methodTwo()==2; } public static void main(String[] args) throws Exception{ OSExecute.command("java com.qiqi.annotations.AtUnit AtUnitExternalTest"); } }
或者你还可以使用组合方式生成非嵌入式测试。
public class AtUnitComposition { AtUnitExample1 testObject = new AtUnitExample1(); @Test boolean _methodOne(){ return testObject.methodOne().equals("This is methodOne"); } @Test boolean _methodTwo(){ return testObject.methodTwo()==2; } public static void main(String[] args) throws Exception{ OSExecute.command("java com.qiqi.annotations.AtUnit AtUnitComposition"); } }

浙公网安备 33010602011771号