02_oo_03_annotation

第十六章 标注(Annotation)

1 简介

标注(Annotation)是一种只读的元数据(Read-only Metadata),它可以像标签"tag"那样“贴”在类(Class)、方法(Method)、和参数(Argument)上,用以表达一项特殊的属性或者能力。标注自身并没有多大的意义,它需要与其他实体联合使用。标签并不会改变程序的运行方式,也不会改变编译器的编译行为,但是,标签能为Java虚拟机(通过反射机制)和编译器(通过标签处理过程Annotation Processing)提供额外的信息。目前,标签的使用非常方便,已被广泛的使用在Java代码库和工程中。

本章将在介绍标注的基础知识之后,还会介绍一些常见的标注的使用方法,和在编译过程中标注的处理过程。最后,讨论一下目前在标注使用方式上的一些争论。

2 Annotation的基础知识

标注(Annotation)是一种特殊的接口(Interface)。标注不支持泛型编程,也不支持继承。但是,Java编译器会为每一个标注设置一个父接口,即java.lang.annotation.Annotation。所以,任何annotation对象都可以赋值给一个Annotation类型的变量。Annotation是由@interface定义的。@和interface分别是两个tokens,所以,Java编译器能很容易的区分Annotation的定义和接口Interface的定义。

根据使用场景,Annotation可被划分为三类,它们是普通标注(Normal Annotation),标记标注(Marker Annotation),和单元素标注(Single Element Annotation)。

2.1 普通标注(Normal Annotation)

普通标注可由多个参数构成,如下面的例子所示,@Developer是一个普通的标注,它包含了author和time两个元素(Element)。在反射机制中,这两个元素可当作成员方法访问。元素的类型可以是基本数据类型(Primitive Type),String,Class,枚举(Enum),或者上述类型的数组。开发人员还可以为每个方法设置一个默认值。例如,在使用Developer标注时,若未设置author的值,则author()方法会返回默认值。

public @interface Developer {
    String author() default "Little Waterdrop";
    String time();
}

2.2 标记标注(Marker Annotation)

标记标注是普通标注的简化版本。标记标注不包含任何成员方法。如果普通标注的每个元素都有默认值的话,普通标注也可以当作标记标注使用。一种常见的标记标注是@NotNull,标注一个参数为非null参数。

public @interface NotNull {
}

2.3 单元素标注(Single Element Annotation)

单元素标注是只含有一个元素的标注。常见的单元素标注有标注名称(例如:@Name)、或者某个特殊的值(例如:@Timeout)。

public @interface Name {
    String name();
}

public @interface Timeout {
    int value();
}

2.4 标注的使用

将标注划分为以上三类的一个主要原因是清晰的区分它们的使用方式。使用普通标注时,需要给每一个元素赋值。标记标记没有任何元素,所以不需要为其元素赋值。因为单元素标注只有一个元素,在赋值时,元素的名称可以省略。如下面的例子所示,@Developer是一个普通标注,所以,在使用时,需要为author和time赋值。但是,author元素设置了默认值,所以,当author元素未设定时,它的值是默认值。@NotNull是标记标注;它标记在setName()方法的输入参数name上,标识入参name不能为null。@Timeout是一个单元素标注。所以,我们可以用@Timeout(value=100)或者@Timeout(100)两种方式给它设置值100。

@Developer (
    time = "2019-11-24"
)
public class AnnotationExample {
    @Developer (time = "2019-11-24")
    private String name = null;

    @Developer(
        author = "Little Waterdrop",
        time = "2019-11-25"
    )
    @Timeout(value=100)
    public void setName(@NotNull String name) {
        this.name = name;
    }
    
    @timeout(100)
    public String getName() {
        return this.name;
    }
}

另一个值得注意的问题是:在哪些位置/对象可以使用标注?标注可以应用于类(Class)、接口(Interface)、枚举(Enum)、标注(Annotation)、类型参数(T)、成员方法(Member Method)、构造函数(Constructor)、成员变量(Member Variable)、异常类型的参数(Exception)和局部变量(Local Variable)。所以,基本上这些地方涵盖了几乎所有能够定义和使用对象的位置。

3 标准标注(Standard Annotations)

Java语言已经为开发人员定义了一些标准标注。标准标注大概可以分为三类,本文着重介绍应用于标注的标注。应用于编译场景的标注比较容易理解,我们仅在表格中总结它们的使用方法。@FunctionInterface是为了函数式编程而引入的概念,我们会在稍后的章节重点讲解。

表一、应用于标注的标准标注。

标注名称应用对象目的
@Target标注(Annotation)指明该标注可以用在哪些实体(例如:类、方法、或者参数)上。
@Retention标注(Annotation)指明该标注的生存期(例如:仅在编译阶段存在)。
@Inherited标注(Annotation)指明如果该标注应用于一个类上,则这个标注自动应用于该类的子类上。
@Repeatable标注(Annotation)指明该标注对同一实体可应用多次。

以下是一些标注使用的例子。

3.1 @Target使用方法

@Target是一个单元素标注。@Target标注的取值可有如下几种。在下面的代码例子中,@Developer的目标是ElementType.TYPE,所以,@Developer可应用于类AnnotationExample上。

  1. ANNOTATION_TYPE:可应用于标注(Annotation)。
  2. CONSTRUCTOR:可应用于构造函数(Constructor)。
  3. FIELD:可应用于成员变量(Field)。
  4. LOCAL_VARIABLE:可应用于局部变量(Local Variable)。
  5. METHOD:可应用于方法(Method),包括标注的元素(Elements of Annotation Type)。
  6. MODULE:可应用于模块(Module)。
  7. PACKAGE:可应用于包(Package)。
  8. PARAMETER:可应用于参数(Parameter)。
  9. TYPE:可应用于类(Class)、接口(Interface)、枚举(Enum)、和标注(Annotation)。
  10. TYPE_PARAMETER:可应用于泛型编程中的类型参数(Type parameter)。
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
public @interface Developer {
    String author() default "Little Waterdrop";
    String time();
}

@Developer(
    author = "Little Waterdrop",
    time = "2019-11-25"
)
public class AnnotationExample {
    private String name = null;

    public void setName(String name) {
        this.name = name;
    }
}

3.2 @Retention使用方法

@Retention是一个单元素标注。其元素的取值可为RetentionPolicy.CLASS, RetentionPolicy.RUNTIME, 或者RetentionPolicy.SOURCE。这几个取值的意义是:

  1. RetentionPolicy.CLASS表示该标注会被Java编译器记录在.class文件中,但是Java虚拟机不会读取该信息,因此,这类标注不能在运行时访问。
  2. RetentionPolicy.RUNTIME表示该标注会被Java编译器记录在.class文件,Java虚拟机也会读取该标注信息。所以,在运行时可以使用反射机制获取此类标注的信息。
  3. RetentionPolicy.SOURCE表示在编译过程中,编译器能读取该标注信息,但是编译器不会把该信息写入.class文件,即Java编译器会丢弃该标注信息。

所以,作为一个完整的例子,@Developer记录的是开发人员的信息。如果该信息仅用在编译过程的话,该标注可设置为RetentionPolicy.CLASS。@NotNull是一个运行时的标注,它用于表示输入参数不能是null。所以,@NotNull应使用RetentionPolicy.RUNTIME。如下例所示。

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.CLASS)
public @interface Developer {
    String author() default "Little Waterdrop";
    String time();
}

@Retention(RetentionPolicy.RUNTIME)
public @interface NotNull {
}

public class AnnotationExample {
    private String name = null;

    @Developer(
        author = "Little Waterdrop",
        time = "2019-11-25"
    )
    public void setName(@NotNull String name) {
        this.name = name;
    }
}

3.3 @Inherited使用方法

@Inherited是一个标识标注。它没有任何元素。当@Inherited标注了一个标注(Annotation)后,这个标注则具备了“继承”的能力,即当该标注应用于一个基类时,该基类的所有子类也应用了这个标注。如下面的例子所示,@Developer标注了@Inherited,所以,当@Developer应用于类AnnotationExample后,它的子类ConcreteAnnotationExample也自动的应用了@Developer。

import  java.lang.annotation.Inherited;

@Inherited
public @interface Developer {
    String author() default "Little Waterdrop";
    String time();
}

@Developer(
        author = "Little Waterdrop",
        time = "2019-11-25"
    )
public class AnnotationExample {
}

public class ConcreteAnnotationExample {

}

3.4 @Repeatable使用方法

@Repeatable也是一个标识标注。它表示一个标注可以应用于一个实体多次。如下面的例子所示,@Developer标识了@Repeatable,所以,在AnnotationExample.setName()方法上,可以应用多次以记录多个开发人员的信息。

import  java.lang.annotation.Repeatable;

@Repeatable
public @interface Developer {
    String author() default "Little Waterdrop";
    String time();
}

public class AnnotationExample {
    private String name = null;

    @Developer(
        author = "Allen",
        time = "2019-11-25"
    )
    @Developer(
        author = "Cathy",
        time = "2019-12-01"
    )
    public void setName(@NotNull String name) {
        this.name = name;
    }
}

3.5 其他标注

表二列出的是用在编译过程中的标准标注。@Override标识一个子类的成员方法覆盖父类的成员方法。@Override可帮助开发人员检查因手误而修改了方法签名的错误。@SuppressWarnings通知编译器不需要报告某一警告(Warning)。@Deprecated用于表示不被推荐使用的类或者方法。@SafeVarargs标注可安全使用的参数。

表二、应用于编译过程的标准标注。

标注名称应用对象目的
@Override成员方法(Method)声明该成员方法覆盖父类或者接口中定义的成员方法。如果成员方法的签名不符合覆盖的要求,编译器会报告错误。
@SuppressWarnings所有,除了包(Package)和标注(Annotation)在编译过程中,Java编译器会检查代码。如果代码可能会导致错误,编译器会报告警告(Warning)。如果开发人员认为代码是正确的,可使用此标注告诉编译器不要报告警告。
@Deprecated所有此标注声明该类、接口、或者方法不被推荐使用。
@SafeVarargs方法(Method)和构造函数(Constructor)断言(Assert)使用该参数是安全的。

@FunctionalInterface为函数式编程提供了有力的支持。 表三、@FunctionalInterface标注。

标注名称应用对象目的
@FunctionalInterface接口(Interface)标注该接口为函数式接口(只有一个抽象方法)。

4 标注的处理过程(Annotation Processing)

在代码开发完毕后,开发人员可以在编译时(At Compile-Time)或者运行时(At Run-Time)获取标注信息,并对其做出相应的处理。本小节重点介绍编译过程中处理标注的方法。运行时获取并处理标注的方法可参考Java反射机制。

当标注使用@Retention(RetentionPolicy.CLASS)或者@Retention(RetentionPolicy.SOURCE)时,该标注在编译过程是可见的。所以,在编译过程中,开发人员能够根据标注的类型和值,对源代码做进一步的处理。这些处理包括自动生成代码、自动生成配置文件、和自动生成脚本文件等。值得注意的是,Java语言允许在编译过程中生成新文件,但是不允许在编译过程中修改已存在的文件。

在编译过程中,Java编译器会扫面代码,找到所有的标注处理器(Annotation Processor),并且依次运行这些标注处理器。如果当一个标注处理器生成了新的文件时,Java编译器会重复上述过程,直到所有的标注处理器都不再生成新的文件。

实际上,标注处理器是javax.annotation.processing.AbstractProcessor的子类,它有一个成员方法process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)。Java编译器会调用这个方法,并传入相应的标注对象。如下面的代码所示,开发人员定义了@Developer,并将其应用于类AnnotationExample。

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.CLASS)
public @interface Developer {
    String author() default "Little Waterdrop";
    String time();
}

@Developer(
        author = "Allen",
        time = "2019-11-25"
    )
public class AnnotationExample {
}

在本例中,DeveloperProcessor类是一个处理Developer标注的处理器。标注@SupportedAnnotationTypes不仅可以接受具体的标注类,还可以使用通配符*。@SupportedSourceVersion表明这个处理器支持哪个版本的Java代码。成员方法process()则处理每个标注。当编译器编译源代码文件时,每当发现标注有@Developer的对象时,就会将其传入process()函数。process()函数的第一个参数是一组标注有@Developer的对象,在process()方法中可以遍历这个集合,以处理每一个标注对象。第二个参数代表着编译器处理标注的轮数。如果process()返回True,则表明传入的标注对象已处理完毕。如果返回False,编译器会将它们传递给下一个标注处理器,继续处理。

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.Element;
import java.util.Set;


@SupportedAnnotationTypes("Developer")
@SupportedSourceVersion(SourceVersion.RELEASE_11)
public class DeveloperProcessor extends AbstractProcessor {
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (TypeElement annotation : annotations) {
            Set<? extends Element> annotatedElements = roundEnv.getElementsAnnotatedWith(annotation);
            // 打印 Annotation Name: Developer
            System.out.println("Annotation Name: " + annotation.getSimpleName().toString());
            for (Element element: annotatedElements) {
                // 打印 Class Name: AnnotationExample
                System.out.println("Class Name: " + element.getSimpleName().toString());
            }
        }
        return true;
    }
}

最后,我们再简单介绍以下如何向Java编译器注册标注处理器,即:Java编译器如何找到标注处理器。最常见的方法有如下几种。

  1. Java编译器javac有一个参数选项 -processor,通过这个参数可以向编译器传递多个标注处理器。
  2. 将标注处理器打包成一个jar文件,并将该jar文件加入到编译器的classpath中。
  3. 使用Maven工具。Maven工具有个插件maven-compiler-plugin提供配置参数<annotationProcessors></annotationProcessors>,可将标注处理器放在里面。

我们用一个例子来展示如何使用第一种方式运行标注处理器。假设我们有三个文件Developer.java,AnnotationExample.java和DeveloperProcessor.java分别定义了上述的@Developer,AnnotationExample类和DeveloperProcessor类。运行如下命令:

javac Developer.java
javac DeveloperProcessor.java
javac AnnotationExample.java -processor DeveloperProcessor

5 标注的常见用例

本小节我们展示两个使用标注的例子。这两个例子分别应用于编译过程和运行时。

5.1 标注在编译时的例子

lombok是一个备受争议的,但是非常流行的第三方代码库。它利用标注,为开发人员自动生成代码。如下例所示。@Getter和@Setter是lombok代码库中使用最多的两个标注。在类Car中定义了一个私有的成员变量brand。因为该变量是私有的,所以,开发人员还需要添加它的getter和setter方法,以便于在其他类中设置和读取brand的值。但是,写getter和setter方法是枯燥的。所以,lombok提供了@Getter和@Setter标注。当@Getter应用于某一成员变量时,在编译过程中,lombok会在该类中自动添加读取该变量的getter方法。当@Setter应用于某一成员变量时,lombok也会在编译过程中为该类自动添加设置该变量的setter方法。这为开发人员节省了大量的时间,提高了开发效率,简化了代码。

lombok的运行机制是,当Java编译器编译代码时,每当查找到使用@Getter或者@Setter标注的成员变量,lombok代码库会向.class文件中添加getter和setter方法。从而,最终获得的.class文件就像是开发人员已为其编写了getter和setter方法一样。

import lombok.Getter;
import lombok.Setter;

public class Car {
    @Getter @Setter
    private String brand = null;

    // lombok.Getter 和 lombok.Setter会自动生成getter和setter方法
    // 所以,开发人员无需再写下面两个枯燥的成员方法。
    // public void setBrand(String brand) {
    //    this.brand = brand;
    //}
    //public String getBrand() {
    //    return this.brand;
    //}
}

5.2 标注在运行时的例子

Jackson是一个流行的Java处理XMLJSON的第三方代码库。Jackson能有效的帮助开发人员在XML/JSON字符串/字符流与普通(POJO: Plain Old Java Object)对象之间相互转换。例如,假如开发人员开发了一个类Car,如下面的例子所示。该类有两个成员变量brand和color。那么,当使用Jackson库中的ObjectMapper将其序列化后,得到的是一个JSON字符串。该字符串包含car对象中的brand和color的成员信息。这是因为Jackson代码库在运行时探查到Car类的两个成员变量应用了@JsonProperty标注,并从标注中获取了用于JSON字符串中key的值。标注在运行时使用的用例还很多,例如:JUnitJetty等。

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;

public class Car {
    @JsonProperty("brand")  // 与成员变量同名时,可省略
    private String brand;

    @JsonProperty("color")  // 与成员变量同名时,可省略
    private String color;

    public Car(String brand, String color) {
        this.brand = brand;
        this.color = color;
    }

    public static void main(String[] args) {
        ObjectMapper objectMapper = new ObjectMapper();
        Car car = new Car("Ford", "Black");
        String carInJson = objectMapper.writeValueAsString(car);

        // 此行打印 {"brand":"Ford","color":"Black"}
        System.out.println(carInJson);
    }
}

6 关于标注使用方式的争论

在Java推出标注(Annotation)特性时,Java语言明确指出标注是一种只读的特性。因此,无论是在编译过程还是运行时,Java程序不能修改程序本身。但是,因为标注得到了开发人员广泛的欢迎,一部分开发人员谨遵“只读”这一特性,将标注用作类、方法、或者变量的只读属性。然而,另一部分开发人员另辟蹊径,根据标注的值,为开发人员自动生成代码、脚本、或者配置文件。例如,上述的lombok代码库,帮助开发人员自动生成辅助性的代码。

诚然,代码自动生成为开发人员节省了大量的时间,减少了代码量。然而,背后的隐患是,因为部分代码是自动生成的。当代码出现问题时,开发人员可能不太容易判断是手写的代码出了问题,还是自动生成的代码出了问题。如果是自动生成的代码出了问题,而且如果没有源代码可参照的话(例如:lombok),问题将会难以定位。

鉴于上述对于标注使用的两种态度。有些公司鼓励使用标注来自动生成代码,而有些公司则明令禁止使用。小水滴则认为,无论标注还是代码自动生成,它们都是工具。孰优孰劣在于使用的人,而非工具。所以,小水滴建议在风险可控的范围内,谨慎使用。

7 结语

本章介绍了标注(Annotation)的基本使用方法和常用的使用场景。标注的出现极大丰富了Java程序开发的方式,是帮助开发人员进行日常开发和维护工作的强大工具。合理的使用标注能极大简化开发和维护的复杂度。许多第三方代码库也使用了标注,简化集成的难度。因此,标注是Java语言极具特色的一种功能强大的特性。

上一章
下一章

注册用户登陆后可留言

Copyright  2019 Little Waterdrop, LLC. All Rights Reserved.