Java Annotation 学习笔记
nbvmfhloj4
9年前
<p> </p> <p>我们平常写Java代码,对其中的注解并不是很陌生,比如说写继承关系的时候经常用到 @Override 来修饰方法。但是 @Override 是用来做什么的,为什么写继承方法的时候要加上它,不加行不行。如果对Java的注解没有了解过,很难回答这些问题。并且,现在越来越多的第三方库开始使用注解,不了解注解的话很难理解他们的逻辑。趁着五一假期,赶紧补习一下什么是注解。</p> <h2>概况</h2> <p>注解是Java5之后引入的新特性,它与 class , interface , enum 处于同一层次。可以理解为在代码中插入一段元数据。它们是在实际的源代码级别保存信息,而不是某种注释性质的文字,这样能够使源代码整洁,便于维护。它可以在三个时期起作用,分别是编译时,构建时和运行时。他们可以在编译时使用预编译工具进行处理,也可以在构建时影响到Ant,Maven等打包工具,还可以在运行期使用反射机制进行处理。</p> <h2>基本用法</h2> <p>不带参数:</p> <pre> <code class="language-java">@Override public void onCreate(Bundle savedInstanceState){ //... } </code></pre> <p>带参数:</p> <pre> <code class="language-java">@CustomizeAnnotation( name = "wakaka", age = 22) //... </code></pre> <p>只有一个参数(可以不指定字段名):</p> <pre> <code class="language-java">@CustomizeAnnotation("wakaka") </code></pre> <h3>java自带的标准注解:</h3> <ul> <li>@Deprecated 标记这个元素被弃用,如果在其它地方对它引用/使用,编译器会发出警告信息。</li> <li>@Override 表示当前的方法覆盖父类中定义的方法。如果不小心拼写错误,或者方法签名对应不上父类的方法,编译器会报出错误提示。</li> <li>@SuppressWarnings 关闭警告信息。</li> </ul> <h3>定义注解</h3> <p>直接上例子:</p> <pre> <code class="language-java">@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface TestAnnotation { } </code></pre> <p>使用方法:</p> <pre> <code class="language-java">public class MainAnnotation { @TestAnnotation public void testMethod() { } } </code></pre> <p>定义注解跟定义接口差不多,只不过关键字要换成 @interface 。定义注解时需要用到 <strong>元注解</strong> ,比如 @Target , @Retention 。 @Target 用来定义注解的使用位置,包括类,方法等; @Retention 定义注解在哪一个级别可用,源代码、类、运行时。</p> <p>注解中一般都包含某些元素来表示某些值。分析注解的时候,主程序或者构建工具可以获取到这些信息。没有元素的注解称为 <strong>标记注解</strong> ,比如说 @Override @Deprecated 。</p> <p>定义注解元素的方式类似于定义接口中的方法,区别在于可以为注解中的元素添加默认值。例子:</p> <pre> <code class="language-java">@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface UseCase { public int id(); public String description() default "no description"; } </code></pre> <p>用法:</p> <pre> <code class="language-java">public class PasswordUtils { @UseCase(id = 47, description = "Password must contain at least one numeric") public boolean validatePassword(String password) { return password.matches("\\w*\\d\\w"); } @UseCase(id = 48) public String encryptPassword(String password) { return new StringBuilder(password).reverse().toString(); } @UseCase(id = 49, description = "New password can't equal previously used ones") public boolean checkForNewPassword(List<String> prevPasswords, String password) { return !prevPasswords.contains(password); } } </code></pre> <p>从上面的例子可以看到,元素的定义类似于方法的定义。方法名就是元素名。使用 default 关键字可以为一个元素增加一个默认值。</p> <p>使用的时候除了带有默认值的元素,需要把所有的元素的值填满。</p> <h3>元注解</h3> <p>Java目前内置了四种 <strong>元注解</strong> 。</p> <ol> <li><strong>@Target</strong><br> 表示该注解可以应用的地方。参数使用 ElementType : <ul> <li><strong>CONSTRUCTOR</strong> 构造器的声明;</li> <li><strong>FIELD</strong> 域声明;</li> <li><strong>LOCAL_VARIABLE</strong> 局部变量的声明;</li> <li><strong>METHOD</strong> 方法声明;</li> <li><strong>PACKAGE</strong> 包的声明;</li> <li><strong>PARAMETER</strong> 参数声明;</li> <li><strong>TYPE</strong> 类、接口、注解、枚举声明;</li> </ul> </li> <li><strong>@Retention</strong><br> 表示需要在什么级别保存该注解信息。参数使用 RetentionPolicy : <ul> <li><strong>SOURCE</strong> 注解将被编译器丢弃;</li> <li><strong>CLASS</strong> 注解在class文件中使用,但是会被VM丢弃;</li> <li><strong>RUNTIME</strong> VM将在运行期也保留注解,因此可以通过反射机制读取注解的信息。</li> </ul> </li> <li><strong>@Documented</strong><br> 将此注解包含在Javadoc中。</li> <li><strong>@Inherited</strong><br> 允许子类继承父类的注解</li> </ol> <p>大多数时候,我们都需要定义自己注解,并编写自己的处理器来处理他们。</p> <h3>注解元素</h3> <p>注解元素可用的类型如下:</p> <ul> <li>所有基本类型(int, boolean, char, long, byte…)</li> <li>String</li> <li>Class</li> <li>enum</li> <li>Annotation</li> <li>以上类型的数组</li> </ul> <p>使用这些类型以外的类型会报错。不允许使用 Integer , Character 等包装类型。</p> <h3>默认值的限制</h3> <ul> <li>所有元素要么有指定的值,要么有默认值;</li> <li>非基本类型的值,无论是指定值还是默认值都不能用 null 。</li> </ul> <h3>注解不支持继承</h3> <p>不能使用 extend 来继承某个 @interface 类型。</p> <h2>编写注解处理器</h2> <p>如果没有读取注解的逻辑,那注解跟注释是差不多的。我们可以利用Java的反射机制构造注解处理器,或者利用工具apt解析带有注解的Java源代码。</p> <p>例子:</p> <pre> <code class="language-java">public class UseCaseTracker { public static void trackUseCases(List<Integer> usecases, Class<?> cl) { for (Method m : cl.getDeclaredMethods()) { UseCase uc = m.getDeclaredAnnotation(UseCase.class); if (uc != null) { System.out.printf("Found Use Case: %d %s\n", uc.id(), uc.description()); usecases.remove(new Integer(uc.id())); } } for (int i : usecases) { System.out.printf("Warning: Missiong use case-%d", i); } } public static void main(String[] args) { List<Integer> useCases = new ArrayList<>(); Collections.addAll(useCases, 47, 48, 49, 50); trackUseCases(useCases, PasswordUtils.class); } } </code></pre> <p>UseCase 注解已经在之前的例子中定义。</p> <p>这个例子功能就是简单比较一下是否缺少一些没有编写的测试用例。其中 useCases 列表包含了所有应当包含的测试用例, PasswodUtils 是所有 UseCase 测试源代码所在的类。</p> <p>检测过程中使用了反射方法 getDeclaredMethods() 和 getDeclaredAnnotation() 。先获取 PasswordUtils 类中的所有方法,并遍历这个列表中的所有方法。如果一个方法被 UseCase 注解修饰,获取这个 UseCase 对象,并取出它的所有元素值。打印UseCase的信息,并在 useCases 中删除这 Usecase 编号。最后打印所有没有编写的用例编号。</p> <h2>生成信息</h2> <p>有些框架除了需要写java代码之外还需要一些额外的配置文件才能协同工作,这种情况最能体现出注解的价值。</p> <p>比如说像 EJB , Hibernate 这样的框架,一般都需要一份xml描述文件。他们提供了Java源文件中类和包的原始信息。如果没有注解,我们在写完java代码之后需要额外再写一份关于Java类的配置问文件。</p> <p>如果我们想添加一个实体类,建立一份基本的的对象/关系的映射,达到自动生成数据库表的目的。我们可以使用注解,它可以清晰的保存在Java源文件中,方便我们了解实体与的关系。</p> <p>例子:</p> <p>数据库中的所有属性都通过注解来传递,所以我们需要定义一些数据库中的‘类型’。这里我们简单的做一个例子,并没有定义全部的属性和类型。</p> <pre> <code class="language-java">//对应数据库中的表, 只有一个属性,表名; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface DBTable { public String name() default ""; } </code></pre> <pre> <code class="language-java">//表中每个字段的约束,只写了3个 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Constraints { boolean primaryKey() default false; boolean allowNull() default true; boolean unique() default false; } </code></pre> <pre> <code class="language-java">//对应数据库中的 INT 类型 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SQLInteger { String name() default ""; Constraints contraints() default @Constraints(); } </code></pre> <pre> <code class="language-java">//对应数据库中的 VARCHAR 类型 @Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface SQLString { int value() default 0; String name() default ""; Constraints constraints() default @Constraints(); } </code></pre> <pre> <code class="language-java">/** * Created by yuxiaofei on 2016/5/7. * 实体类,被注解修饰的成员变量会被加入到数据库中 */ @DBTable(name = "Member") public class Member { @SQLString(value = 30) String firstName; @SQLString(value = 50) String lastName; @SQLInteger Integer age; @SQLString(value = 30, constraints = @Constraints(primaryKey = true)) String handle; static int memberCount; } </code></pre> <pre> <code class="language-java">/** * Created by yuxiaofei on 2016/5/7. * 根据目标实体类,声称创建表的SQL */ public class TableCreator { public static void main(String[] args) { Class<?> targetClass = Member.class; DBTable dbTable = targetClass.getAnnotation(DBTable.class); if (dbTable == null) { System.out.printf("No DBTable in class %s. \n", targetClass.getSimpleName()); System.exit(-1); } String tableName = dbTable.name(); if (tableName.length() < 1) { //默认名使用类名的全字母全大写 tableName = targetClass.getName().toUpperCase(); } List<String> columnDefs = new ArrayList<String>(); getColumnDefs(targetClass, columnDefs); String SQL = createSQL(tableName, columnDefs); System.out.println(SQL); } //根据表名和字段声明,生成创建表的SQL private static String createSQL(String tableName, List<String> columnDefs) { StringBuilder sb = new StringBuilder(); sb.append(String.format("CREATE TABLE %s (\n", tableName)); for (int i = 0; i < columnDefs.size(); i++) { String column = columnDefs.get(i); if (i != columnDefs.size() - 1) { sb.append(String.format(" %s,\n", column)); } else { sb.append(String.format(" %s\n);", column)); } } return sb.toString(); } /** * 根据实体类生成创建表的字段 * * @param targetClass 目标实体类 * @param columnDefs 字段声明列表 */ private static void getColumnDefs(Class<?> targetClass, List<String> columnDefs) { for (Field field : targetClass.getDeclaredFields()) { String columnName = null; Annotation[] anns = field.getDeclaredAnnotations(); if (anns.length < 1) { continue;//没有被注解修饰,非数据库表内字段 } if (anns[0] instanceof SQLInteger) { SQLInteger sqlInteger = (SQLInteger) anns[0]; if (sqlInteger.name().length() < 1) {//默认名,用变量名全大写形式代替 columnName = field.getName().toUpperCase(); } else { columnName = sqlInteger.name(); } Constraints constraints = sqlInteger.contraints(); columnDefs.add(String.format("%s INT %s", columnName, getConstraints(constraints))); } else if (anns[0] instanceof SQLString) { SQLString sqlString = (SQLString) anns[0]; if (sqlString.name().length() < 1) {//默认名用变量名全字母大写 columnName = field.getName().toUpperCase(); } else { columnName = sqlString.name(); } Constraints constraints = sqlString.constraints(); columnDefs.add( String.format( "%s VARCHAR(%d) %s", columnName, sqlString.value(), getConstraints(constraints) ) ); } } } /** * 根据注解中的配置声称字段的约束 * * @param constraints 注解约束配置 * @return 字段约束 */ private static String getConstraints(Constraints constraints) { StringBuilder sb = new StringBuilder(); if (!constraints.allowNull()) { sb.append(" NOT NULL"); } if (constraints.primaryKey()) { sb.append(" PRIMARY KEY"); } if (constraints.unique()) { sb.append(" UNIQUE"); } return sb.toString(); } } </code></pre> <p>例子比较简单,运行一下就可以看到结果。</p> <p>对于注解的学习就到这里了,有什么疑问可以在回复中一起交流。</p> <p>来自: http://blog.cwzyzl.cn/2016/05/08/Java-Annotation-学习笔记-md/</p>