从 JDK 1.5 版本开始,Java 语言提供了通用的 Annotation 功能,允许开发者定义和使用自己的 Annotation 类型。Annotation 功能包括了关于定义 Annotation 类型的语法,对声明式进行注解的语法,读取 Annotation 的 API,Annotation 在 class 文件中的表现,以及 Annotation 处理工具(APT)。
Annotation 并不直接对程序的语法产生作用,但是会提供一些程序之外的数据或者信息,影响工具或者类库对程序的处理或者调用的方式,从而最终影响程序运行时的行为。
Annotation 功能现在已经广泛应用于各种基于 Java 的应用系统的开发。Java API 本身就预定义了一系列的 Annotation 类型,例如 @Deprecated, @Override, @SuppressWarnings 等等。在流行的框架中也经常会用到各种 Annotation, 如 Spring 中的 @Required,@Autowired,@Aspect,@Pointcut 等等。
因此对于 Java 开发者来说,很可能已经通过使用 Annotation 来对代码和软件质量进行了提升。以 @Override 标签为例,在具有复杂继承结构的大型项目中,对于父类的某一个方法,开发者很难直观地通过代码判断出是哪一个子类的具体实现在运行时被调用。而一旦开发者改变了父类的方法名或其参数,而忽视了对子类作出同样的修改,子类中重写的方法将不会再被调用,从而导致系统无法呈现出预期的行为或结果。通过引入 @Override 标签,Java 编译器在对相关代码进行编译时会提示开发者对所有相关的子类中的重写方法进行同步修改,从而避免这一问题的产生。
为了更好地帮助开发者提升代码的质量和可读性,以及自动化代码分析的准确性,Java 8 对 Annotation 引入了两项重要的改变:Type Annotation 和 Repeating Annotation。
回页首
在 Java 8 之前的版本中,只能允许在声明式前使用 Annotation。而在 Java 8 版本中,Annotation 可以被用在任何使用 Type 的地方,例如:初始化对象时 (new),对象类型转化时,使用 implements 表达式时,或者使用 throws 表达式时。
清单 1. Type Annotation 使用示例
//初始化对象时 String myString = new @NotNull String(); //对象类型转化时 myString = (@NonNull String) str; //使用 implements 表达式时 class MyList<T> implements @ReadOnly List<@ReadOnly T>{ ... } //使用 throws 表达式时 public void validateValues() throws @Critical ValidationFailedException{ ... }
定义一个 Type Annotation 的方法与普通的 Annotation 类似,只需要指定 Target 为 ElementType.TYPE_PARAMETER 或者 ElementType.TYPE_USE,或者同时指定这两个 Target。
清单 2. 定义 Type Annotation 示例
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE}) public @interface MyAnnotation { }
ElementType.TYPE_PARAMETER 表示这个 Annotation 可以用在 Type 的声明式前,而 ElementType.TYPE_USE 表示这个 Annotation 可以用在所有使用 Type 的地方(如:泛型,类型转换等)
与 Java 8 之前的 Annotation 类似的是,Type Annotation 也可以通过设置 Retention 在编译后保留在 class 文件中(RetentionPolicy.CLASS)或者运行时可访问(RetentionPolicy.RUNTIME)。但是与之前不同的是,Type Annotation 有两个新的特性:在本地变量上的 Annotation 可以保留在 class 文件中,以及泛型类型可以被保留甚至在运行时被访问。
虽然 Type Annotation 可以保留在 class 文件中,但是它并不会改变程序代码本身的行为。例如在一个方法前加上 Annotation,调用此方法返回的结果和不加 Annotation 的时候一致。
Java 8 通过引入 Type Annotation,使得开发者可以在更多的地方使用 Annotation,从而能够更全面地对代码进行分析以及进行更强的类型检查。
在实际应用中,可能会出现需要对同一个声明式或者类型加上相同的 Annotation(包含不同的属性值)的情况。
例如系统中除了管理员之外,还添加了超级管理员这一权限,对于某些只能由这两种角色调用的特定方法,可以使用 Repeating Annotation。
清单 3. Repeating Annotation 使用示例-1
@Access(role="SuperAdministrator") @Access(role="Administrator") public void doCheck() { ...... }
上面的示例是针对方法使用 Annotation, 开发者也可以根据产品中的具体需求在其他地方使用 Repeating Annotation。例如某个类专门提供管理员相关的功能,可以直接在这个类上标注同样的 Annotation。
清单 4. Repeating Annotation 使用示例-2
@Access(role="SuperAdministrator") @Access(role="Administrator") public class AdminServices{ }
之前版本的 JDK 并不允许开发者在同一个声明式前加注同样的 Annotation,(即使属性值不同)这样的代码在编译过程中会提示错误。而 Java 8 解除了这一限制,开发者可以根据各自系统中的实际需求在所有可以使用 Annotation 的地方使用 Repeating Annotation。
由于兼容性的缘故,Repeating Annotation 并不是所有新定义的 Annotation 的默认特性,需要开发者根据自己的需求决定新定义的 Annotation 是否可以重复标注。Java 编译器会自动把 Repeating Annotation 储存到指定的 Container Annotation 中。而为了触发编译器进行这一操作,开发者需要进行以下的定义:
首先,在需要重复标注特性的 Annotation 前加上 @Repeatable 标签,示例如下:
清单 5. 定义 Repeating Annotation 示例
@Repeatable(AccessContainer.class) public @interface Access { String role(); }
@Repeatable 标签后括号中的值即为指定的 Container Annotation 的类型。在这个例子中,Container Annotation 的类型是 AccessContainer,Java 编译器会把重复的 Access 对象保存在 AccessContainer 中。
AccessContainer 中必须定义返回数组类型的 value 方法。数组中元素的类型必须为对应的 Repeating Annotation 类型。具体示例如下:
清单 6. 定义 Container Annotation 示例
public @interface AccessContainer { Access[] value(); }
可以通过 Java 的反射机制获取注解的 Annotation。一种方式是通过 AnnotatedElement 接口的 getAnnotationByType(Class<T>) 首先获得 Container Annotation,然后再通过 Container Annotation 的 value 方法获得 Repeating Annotation。另一种方式是用过 AnnotatedElement 接口的 getAnnotations(Class<T>) 方法一次性返回 Repeating Annotation。
Repeating Annotation 使得开发者可以根据具体的需求对同一个声明式或者类型加上同一类型的注解,从而增加代码的灵活性和可读性。
回页首
Java 8 通过引入 Annotation 的新特性以支持对 Java 程序代码进行更全面和深入的分析和校验。然而 Java 8 本身并没有提供校验框架,要针对 Annotation 进行代码分析,开发者可以选择一些第三方工具,例如:Checker Framework, FindBugs, Eclipse, IntelliJ 等开源工具,以及一些其他的商业分析工具。这些工具可以在 IDE, Maven/Ant 或者持续集成平台中使用。
相比于其他的开源工具,Checker Framework 对 Java 8 的兼容性最好,包括支持注释中的 Annotation。它本身内置了针对一系列常见错误类型的校验类,如空指针,字符串格式不匹配,度量单位不匹配,安全漏洞,并发错误等。同时开发者也可以自定义校验类型。本文中选择使用 Checker Framework 对代码进行基于 Annotation 的分析和校验。
如果开发者使用支持 Java 8 的 Eclipse 版本(如 4.4.0)进行开发,可以直接在 Eclipse 中安装 Checker Framework 插件。步骤如下:
Checker Framework 提供了一系列内置的校验类,具体如下:
表 1. Checker Framework 内置校验类
名称 | 功能 |
---|---|
Nullness Checker | 检测和避免系统出现空指针异常。 |
Javari Checker | 检测和避免只读对象的值被修改。 |
Interning Checker | 保证引用比较操作符“==”的正确使用,即是,“==”没有用在本应该使用 equals() 方法的地方。 |
Fenum Checker | 全名为 Fake Enum Checker,用来保证使用正确的 Fake Enum 常量。 Fake Enum: 也就是用来替代 Enum 使用的 int 或者 String 常量定义, 例如 public static final @Fenum("A") int ACONST1 = 1 ;。 |
Format String Checker | 检测和避免在格式化方法(例如 System.out.printf()或者 String.format())中使用不正确格式的 String。 |
Linear Checker | 保证线性类型系统,避免别名的使用,即一个对象只能同时存在一个引用。 |
Lock Checker | 检测和保证线程在访问被标注的对象或方法时持有合适的锁。 |
Regex Checker | 检测和保证 String 符合正则表达式。 |
Tainting Checker | 检测数据源参数是否可能为脏数据。 |
I18n Checker | 检测代码中的国际化是否正确使用,即字符串或对象是否对应国际化文件中的 key 或者是否与选定的 locale 一致。 |
以 Nullness Checker 为例,要使用 Checker Framework 对代码进行分析,首先在代码中需要做空指针校验的对象或方法上标注相应的 Annotation, 如以下示例:
清单 7. Nullness Checker 示例-1
class NullnessExample { @Nullable Object o1; @NonNull Object o2; @EnsuresNonNullIf(expression="getComponentType()", result=true) public native boolean isArray(); public native @Nullable Class<?> getComponentType(); }
要执行相应的校验插件,只需要在用 Javac 进行正常编译时,加上 -processor plugin_class 参数。具体示例如下:
清单 8. Nullness Checker 示例-2
javac -processor org.checkerframework.checker.nullness.NullnessChecker NullnessExample.java
在 Eclipse 里使用 Checker Framework 插件则可以直接在需要做校验的类或者项目的右键选项中选择 Checker Framework -> Run Built-in Checker -> Nullness Checker。
编译完成后即可以在日志中找到相应的提示信息。例如:
清单 9. Nullness Checker 日志
Description: dereference of possibly-null reference o1 Resource: NullnessExample.java Path: /Mysamples/src/checkerFramework/samples Location: 26 Type:Checker Framework Problem
开发者可以通过这些信息快速准确地定位代码中潜在的缺陷,从而避免程序运行时出现异常。
回页首
Type Annotation 为 Java 程序中的校验提供了更多的可能性,然而,相对于业务方面的校验,它更适用于计算机科学相关的一般性的格式校验。例如,空指针校验,数值范围校验,字符串格式校验等,都可以很方便地通过使用 Type Annotation 结合第三方的校验工具,如 Checker Framework 进行校验,避免程序在运行时出现此类错误或异常。
然而一些特定的业务逻辑方面的校验可能并不适合通过 Type Annotation 进行实现。例如在做转账交易时,需要检查用户权限以及账户余额等,这类的校验需要涉及各种不同的业务数据,很难通过一般性的校验框架执行校验。
回页首
在 Java 8 中,Annotation 得到了很好的扩展,可以用在任何使用 Type 的地方,而不仅限于声明式前。Annotation 本身并不会改变程序的运行行为,但是可以通过使用第三方工具,如 Checker Framework, 开发者可以利用 Annotation 去自动化地对程序中的潜在缺陷进行检测,提升软件质量,避免重复开发,提高开发效率。