Skip to content

【OnJava-二十三】注解

基本语法

在下面的例子中,使用 @Test 对 testExecute() 进行注解。该注解本身不做任何事情,但是编译器要保证其类路径上有 @Test 注解的定义。你将在本章看到,我们通过注解创建了一个工具用于运行这个方法:

java
package annotations;
import onjava.atunit.*;
public class Testable {
    public void execute() {
        System.out.println("Executing..");
    }
    @Test
    void testExecute() { execute(); }
}
package annotations;
import onjava.atunit.*;
public class Testable {
    public void execute() {
        System.out.println("Executing..");
    }
    @Test
    void testExecute() { execute(); }
}

定义注解

如下是一个注解的定义。注解的定义看起来很像接口的定义。事实上,它们和其他 Java 接口一样,也会被编译成 class 文件。

java
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}
package onjava.atunit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Test {}

除了 @ 符号之外, @Test 的定义看起来更像一个空接口。
注解的定义也需要一些元注解(meta-annoation),比如 @Target 和 @Retention

  • @Target 定义你的注解可以应用在哪里(例如是方法还是字段)。
  • @Retention 定义了注解在哪里可用,在源代码中(SOURCE),class 文件(CLASS)中或者是在运行时(RUNTIME)。

注解通常会包含一些表示特定值的元素。当分析处理注解的时候,程序或工具可以利用这些值。注解的元素看起来就像接口的方法,但是可以为其指定默认值。

不包含任何元素的注解称为标记注解(marker annotation),例如上例中的 @Test 就是标记注解。

下面是一个简单的注解,我们可以用它来追踪项目中的用例。程序员可以使用该注解来标注满足特定用例的一个方法或者一组方法。于是,项目经理可以通过统计已经实现的用例来掌控项目的进展,而开发者在维护项目时可以轻松的找到用例用于更新,或者他们可以调试系统中业务逻辑。

java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();
    String description() default "no description";
}

...
// 使用
@UseCase(id = 0, description = "hello")  
protected int age() {  
    return 10;  
}
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UseCase {
    int id();
    String description() default "no description";
}

...
// 使用
@UseCase(id = 0, description = "hello")  
protected int age() {  
    return 10;  
}

元注解

Java 语言中目前有 5 种标准注解(前面介绍过),以及 5 种元注解。元注解用于注解其他的注解

  • @Target
    • 表示注解可以用于哪些地方。
    • 可能的 ElementType 参数包括:
      • CONSTRUCTOR:构造器的声明
      • FIELD:字段声明(包括 enum 实例)
      • LOCAL_VARIABLE:局部变量声明
      • METHOD:方法声明
      • PACKAGE:包声明
      • PARAMETER:参数声明
      • TYPE:类、接口(包括注解类型)或者 enum 声明
  • @Retention
    • 表示注解信息保存的时长。
    • RetentionPolicy 参数包括:
      • SOURCE:注解将被编译器丢弃
      • CLASS:注解在 class 文件中可用,但是会被 VM 丢弃。
      • RUNTIME:VM 将在运行期也保留注解,因此可以通过反射机制读取注解的信息。
  • @Document
    • 将此注解保存在 Javadoc 中
  • @Interited
    • 允许子类继承父类的注解
  • @Repeatable
    • 允许一个注解可以被使用一次或者多次(Java 8)

编写注解处理器

如果没有用于读取注解的工具,那么注解不会比注释更有用。使用注解中一个很重要的部分就是,创建与使用注解处理器。Java 拓展了反射机制的 API 用于帮助你创造这类工具。同时他还提供了 javac 编译器钩子在编译时使用注解。

下面是一个非常简单的注解处理器,我们用它来读取被注解的 PasswordUtils 类,并且使用反射机制来寻找 @UseCase 标记。给定一组 id 值,然后列出在 PasswordUtils 中找到的用例,以及缺失的用例。

java
public class PasswordUtils {  
    @UseCase(id = 47, description =  
            "Passwords must contain at least one numeric")  
    public boolean validatePassword(String passwd) {  
        return (passwd.matches("\\w*\\d\\w*"));  
    }  
    @UseCase(id = 48)  
    public String encryptPassword(String passwd) {  
        return new StringBuilder(passwd)  
                .reverse().toString();  
    }  
    @UseCase(id = 49, description =  
            "New passwords can't equal previously used ones")  
    public boolean checkForNewPassword(  
            List<String> prevPasswords, String passwd) {  
        return !prevPasswords.contains(passwd);  
    }  
}
public class PasswordUtils {  
    @UseCase(id = 47, description =  
            "Passwords must contain at least one numeric")  
    public boolean validatePassword(String passwd) {  
        return (passwd.matches("\\w*\\d\\w*"));  
    }  
    @UseCase(id = 48)  
    public String encryptPassword(String passwd) {  
        return new StringBuilder(passwd)  
                .reverse().toString();  
    }  
    @UseCase(id = 49, description =  
            "New passwords can't equal previously used ones")  
    public boolean checkForNewPassword(  
            List<String> prevPasswords, String passwd) {  
        return !prevPasswords.contains(passwd);  
    }  
}
java
import java.util.*;
import java.util.stream.*;
import java.lang.reflect.*;
public class UseCaseTracker {
    public static void
    trackUseCases(List<Integer> useCases, Class<?> cl) {
        for(Method m : cl.getDeclaredMethods()) {
            UseCase uc = m.getAnnotation(UseCase.class);
            if(uc != null) {
                System.out.println("Found Use Case " +
                        uc.id() + "\n " + uc.description());
                useCases.remove(Integer.valueOf(uc.id()));
            }
        }
        useCases.forEach(i ->
                System.out.println("Missing use case " + i));
    }
    public static void main(String[] args) {
        List<Integer> useCases = IntStream.range(47, 51)
                .boxed().collect(Collectors.toList());
        trackUseCases(useCases, PasswordUtils.class);
    }
}
import java.util.*;
import java.util.stream.*;
import java.lang.reflect.*;
public class UseCaseTracker {
    public static void
    trackUseCases(List<Integer> useCases, Class<?> cl) {
        for(Method m : cl.getDeclaredMethods()) {
            UseCase uc = m.getAnnotation(UseCase.class);
            if(uc != null) {
                System.out.println("Found Use Case " +
                        uc.id() + "\n " + uc.description());
                useCases.remove(Integer.valueOf(uc.id()));
            }
        }
        useCases.forEach(i ->
                System.out.println("Missing use case " + i));
    }
    public static void main(String[] args) {
        List<Integer> useCases = IntStream.range(47, 51)
                .boxed().collect(Collectors.toList());
        trackUseCases(useCases, PasswordUtils.class);
    }
}

注解元素

在 UseCase.java 中定义的 @UseCase 的标签包含 int 元素 id 和 String 元素 description。注解元素可用的类型如下所示:

  • 所有基本类型(int、float、boolean 等)
  • String
  • Class
  • enum
  • Annotation
  • 以上类型的数组

如果你使用了其他类型,编译器就会报错。注意,也不允许使用任何包装类型,但是由于自动装箱的存在,这不算是什么限制。注解也可以作为元素的类型。稍后你会看到,注解嵌套是一个非常有用的技巧。

默认值限制

编译器对于元素的默认值有些过于挑剔。首先,元素不能有不确定的值。也就是说,元素要么有默认值,要么就在使用注解时提供元素的值

这里有另外一个限制:任何非基本类型的元素, 无论是在源代码 声明时还是在注解接口中定义默认值时,都不能使用 null 作为其值。这个限制使得处理器很难表现一个元素的存在或者缺失的状态,因为在每个注解的声明中,所有的元素都存在,并且具有相应的值。为了绕开这个约束,可以自定义一些特殊的值,比如空字符串或者负数用于表达某个元素不存在

java
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
    int id() default -1;
    String description() default "";
}
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SimulatingNull {
    int id() default -1;
    String description() default "";
}

生成外部文件

描述一个工具想法,通过实体zhu'jie,执行sql

当有些框架需要一些额外的信息才能与你的源代码协同工作,这种情况下注解就会变得十分有用。像 Enterprise JavaBeans (EJB3 之前)这样的技术,每一个 Bean 都需要需要大量的接口和部署描述文件,而这些就是 “样板” 文件。Web Service,自定义标签库以及对象 / 关系映射工具(例如 Toplink 和 Hibernate)通常都需要 XML 描述文件,而这些文件脱离于代码之外。除了定义 Java 类,程序员还必须忍受沉闷,重复的提供某些信息,例如类名和包名等已经在原始类中已经提供的信息。每当你使用外部描述文件时,他就拥有了一个类的两个独立信息源,这经常导致代码的同步问题。同时这也要求了为项目工作的程序员在知道如何编写 Java 程序的同时,也必须知道如何编辑描述文件。

假设你想提供一些基本的对象 / 关系映射功能,能够自动生成数据库表。你可以使用 XML 描述文件来指明类的名字、每个成员以及数据库映射的相关信息。但是,通过使用注解,你可以把所有信息都保存在 JavaBean 源文件中。为此你需要一些用于定义数据库名称、数据库列以及将 SQL 类型映射到属性的注解。

以下是一个注解的定义,它告诉注解处理器应该创建一个数据库表:

java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    String name() default "";
}
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME)
public @interface DBTable {
    String name() default "";
}

在 @Target 注解中指定的每一个 ElementType 就是一个约束,它告诉编译器,这个自定义的注解只能用于指定的类型。你可以指定 enum ElementType 中的一个值,或者以逗号分割的形式指定多个值。如果想要将注解应用于所有的 ElementType,那么可以省去 @Target 注解,但是这并不常见。

注意 @DBTable 中有一个 name() 元素,该注解通过这个元素为处理器创建数据库时提供表的名字。

java
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    int value() default 0;
    String name() default "";
    Constraints constraints() default @Constraints;
}

package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints;
}
package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Constraints {
    boolean primaryKey() default false;
    boolean allowNull() default true;
    boolean unique() default false;
}

package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLString {
    int value() default 0;
    String name() default "";
    Constraints constraints() default @Constraints;
}

package annotations.database;
import java.lang.annotation.*;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface SQLInteger {
    String name() default "";
    Constraints constraints() default @Constraints;
}

使用 javac 处理注解

通过 javac,你可以通过创建编译时(compile-time)注解处理器在 Java 源文件上使用注解,而不是编译之后的 class 文件。但是这里有一个重大限制:你不能通过处理器来改变源代码。唯一影响输出的方式就是创建新的文件。

如果你的注解处理器创建了新的源文件,在新一轮处理中注解会检查源文件本身。工具在检测一轮之后持续循环,直到不再有新的源文件产生。然后它编译所有的源文件。

每一个你编写的注解都需要处理器,但是 javac 可以非常容易的将多个注解处理器合并在一起。你可以指定多个需要处理的类,并且你可以添加监听器用于监听注解处理完成后接到通知。

本节中的示例将帮助你开始学习,但如果你必须深入学习,请做好反复学习,大量访问 Google 和 StackOverflow 的准备。

最简单的处理器

让我们开始定义我们能想到的最简单的处理器,只是为了编译和测试。如下是注解的定义:

java
package annotations.simplest;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD,
        ElementType.CONSTRUCTOR,
        ElementType.ANNOTATION_TYPE,
        ElementType.PACKAGE, ElementType.FIELD,
        ElementType.LOCAL_VARIABLE})
public @interface Simple {
    String value() default "-default-";
}
package annotations.simplest;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE, ElementType.METHOD,
        ElementType.CONSTRUCTOR,
        ElementType.ANNOTATION_TYPE,
        ElementType.PACKAGE, ElementType.FIELD,
        ElementType.LOCAL_VARIABLE})
public @interface Simple {
    String value() default "-default-";
}

@Retention 的参数现在为 SOURCE,这意味着注解不会再存留在编译后的代码。这在编译时处理注解是没有必要的,它只是指出,在这里,javac 是唯一有机会处理注解的代理。

@Target 声明了几乎所有的目标类型(除了 PACKAGE) ,同样是为了演示。下面是一个测试示例。

java
package annotations.simplest;
@Simple
public class SimpleTest {
    @Simple
    int i;
    @Simple
    public SimpleTest() {}
    @Simple
    public void foo() {
        System.out.println("SimpleTest.foo()");
    }
    @Simple
    public void bar(String s, int i, float f) {
        System.out.println("SimpleTest.bar()");
    }
    @Simple
    public static void main(String[] args) {
        @Simple
        SimpleTest st = new SimpleTest();
        st.foo();
    }
}
package annotations.simplest;
@Simple
public class SimpleTest {
    @Simple
    int i;
    @Simple
    public SimpleTest() {}
    @Simple
    public void foo() {
        System.out.println("SimpleTest.foo()");
    }
    @Simple
    public void bar(String s, int i, float f) {
        System.out.println("SimpleTest.bar()");
    }
    @Simple
    public static void main(String[] args) {
        @Simple
        SimpleTest st = new SimpleTest();
        st.foo();
    }
}

如果只是通过平常的方式来编译 SimpleTest.java,你不会得到任何结果。为了得到注解输出,你必须增加一个 processor 标志并且连接注解处理器类

javac -processor annotations.simplest.SimpleProcessor SimpleTest.java
javac -processor annotations.simplest.SimpleProcessor SimpleTest.java

没有跑通,下面是书里的结果:

bash
annotations.simplest.Simple
==== annotations.simplest.SimpleTest ====
CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest
annotations.simplest.SimpleTest
java.lang.Object
i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])
==== i ====
FIELD : [] : i : int
==== SimpleTest() ====
CONSTRUCTOR : [public] : <init> : ()void
==== foo() ====
METHOD : [public] : foo : ()void
void foo()
==== bar(java.lang.String,int,float) ====
METHOD : [public] : bar : (java.lang.String,int,float)void
void bar(s,i,f)
==== main(java.lang.String[]) ====
METHOD : [public, static] : main : (java.lang.String[])void
void main(args)
annotations.simplest.Simple
==== annotations.simplest.SimpleTest ====
CLASS : [public] : SimpleTest : annotations.simplest.SimpleTest
annotations.simplest.SimpleTest
java.lang.Object
i,SimpleTest(),foo(),bar(java.lang.String,int,float),main(java.lang.String[])
==== i ====
FIELD : [] : i : int
==== SimpleTest() ====
CONSTRUCTOR : [public] : <init> : ()void
==== foo() ====
METHOD : [public] : foo : ()void
void foo()
==== bar(java.lang.String,int,float) ====
METHOD : [public] : bar : (java.lang.String,int,float)void
void bar(s,i,f)
==== main(java.lang.String[]) ====
METHOD : [public, static] : main : (java.lang.String[])void
void main(args)