Java-注解详解

467 阅读17分钟

注解的含义

1、Annotation 是一种应用于包、类型、构造方法、方法、成员变量、参数、本地变量的声明中的特殊修饰符。 这些信息被存储在 Annotation 的 “name=value” 结构对中。

2、Annotation会被编译器直接忽略,注解则可以被编译器打包进入class文件,因此,注解是一种用作标注的“元数据”,即数据的数据。

3、从JVM的角度看,Annotation本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。

public @interface 注解名 {
     定义体
}

注解分类

名称含义示例
由编译器使用的注解这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了@Override:让编译器检查该方法是否正确地实现了覆写;@SuppressWarnings:告诉编译器忽略此处代码产生的警告。
由工具处理.class文件使用的注解有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。可以参考lombok
在程序运行期能够读取的注解它们在加载后一直存在于JVM中,这也是最常用的注解一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。

元注解(java.lang.annotation下的注解:用来定义注解的)

@Retention

@Retention:注解的保留策略。该注解指明了被它所注解的注解被保留的时间长短。

@Retention 包含一个名为 value 的成员变量,该 value 成员变量是 RetentionPolicy,RetentionPolicy 是枚举类型,值有如下几个:

RetentionPolicy.SOURCE:注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视,不记录在 class 文件中。

RetentionPolicy.CLASS:注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。这是默认行为,所有没有用 @Retention 注解的注解,都会采用这种策略。

RetentionPolicy.RUNTIME:注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,程序可以通过反射获取该注解的信息。

@Target

@Target:注解的作用目标。该注解指定注解用于修饰哪些程序元素。

@Target 也包含一个名为 value 的成员变量,该 value 成员变量类型为 ElementType[],ElementType 也为枚举类型,值有如下几个:

ElementType.TYPE: 修饰类型,比如接口、类、枚举、注解

ElementType.FIELD: 修饰属性,比如成员变量、枚举常量

ElementType.METHOD: 修饰方法

ElementType.PARAMETER:修饰方法内的参数

ElementType.CONSTRUCTOR:修饰构造方法

ElementType.LOCAL_VARIABLE:修饰局部变量

ElementType.ANNOTATION_TYPE:修饰注解

ElementType.PACKAGE:修饰包

ElementType.TYPE_PARAMETER:修饰类型参数(Java8 新增)

ElementType.TYPE_USE:修饰任何类型(Java8 新增)

@Inherited

@Inherited:指定注解具有继承性。但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。

@Documented

@Documented:注解将被包含在 Javadoc 中。该注解的作用是在用 Javadoc 命令生成 API 文档时能够将注解中的元素包含到 Javadoc 中去。

@Repeatable (Java8新增)

@Repeatable: 表示可重复注解。@Repeatable 是 Java 8 才加进来的,所以算是一个新的特性。

@Target({ElementType.METHOD, ElementType.TYPE}) // 修饰方法并修饰类型
@Retention(RetentionPolicy.RUNTIME) // 注解可以保留到程序运行的时候,这种是我们最常用的
@Documented
@Mapping
public @interface RequestMapping{
    ......
}

JDK 内置注解(都属于由编译器使用的注解)

在 java.lang 包下,JDK 提供了 5 个基本注解:@Override、@Deprecated、@SuppressWarnings、@SafeVarargs、@FunctionalInterface。

@Override

@Override 用于标注重写了父类的方法。

当我们想要复写父类中的方法时,我们需要使用该注解去告知编译器我们想要复写这个方法,这样一来当父类中的方法移除或者发生更改时编译器将提示错误信息。对于子类中被 @Override 修饰的方法,如果存在对应的被重写的父类方法,则正确;如果不存在,则报错。@Override 只能作用于方法,不能作用于其他程序元素。

@Deprecated

@Deprecated 用于表示某个程序元素(类、方法等)已过时。

当我们希望编译器知道某一方法不建议使用时,我们应该使用这个注解。Java 在 javadoc 中推荐使用该注解,我们应该提供为什么该方法不推荐使用以及替代的方法。如果使用了被 @Deprecated 修饰的类或方法等,编译器会发出警告。

@SuppressWarnings

@SuppressWarnings 用于抑制编译器的警告。

这个仅仅是告诉编译器忽略特定的警告信息,例如在泛型中使用原生数据类型。指示被@SuppressWarnings 修饰的程序元素(以及该程序元素中的所有子元素,例如类以及该类中的方法)取消显示指定的编译器警告。例如,常见的

@SuppressWarnings("unchecked")。

@SuppressWarnings 注解的常见参数值主要有以下几种:

deprecation:使用了不赞成使用的类或方法时的警告(使用 @Deprecated 使得编译器产生的警告);

unchecked:执行了未检查的转换时的警告,例如当使用集合时没有用泛型 (Generics) 来指定集合保存的类型; 关 闭编译器警告

fallthrough:当 switch 程序块直接通往下一种情况而没有 break 语句时的警告;

path:在类路径、源文件路径等中有不存在的路径时的警告;

serial:当在可序列化的类上缺少 serialVersionUID 定义时的警告;

finally:任何 finally 子句不能正常完成时的警告;

all:关于以上所有情况的警告。

@SafeVarargs

@SafeVarargs 是 JDK 7 专门为抑制堆污染警告提供的。

@FunctionalInterface (Java8新增)

@FunctionalInterface 是 Java8 中新增的函数式接口。Java8 规定:如果接口中只有一个抽象方法(可以包含多个 default 方法或多个 static 方法),该接口称为函数式接口。

注意:
value 特权:如果使用注解时只需要为 value 成员变量指定值,则使用注解时可以直接在该注解的括号中指定 value 值
,而无需使用 name=value 的形式(如@SuppressWarnings("unchecked"))。
坚持使用 @Override 注解:如果在每个方法中使用 @Override 注解来声明要覆盖父类声明,编译器就可以替你防止大量的错误。

自定义注解

使用 @interface 自定义注解时,自动继承了 java.lang.annotation.Annotation 接口,由编译程序自动完成其他细节。

在定义注解时,不能继承其他的注解或接口。@interface 用来声明一个注解,其中的每一个方法实际上是声明了一个配置参数。

方法的名称就是参数的名称,返回值类型就是参数的类型(返回值类型只能是基本类型、Class、String、Enum)。可以通过 default 来声明参数的默认值。

注解参数的可支持数据类型:

所有基本数据类型(int、float、boolean、byte、double、char、long、short)

String 类型

Class 类型

Enum 类型

Annotation 类型

以上所有类型的数组

定义新注解使用 @interface 关键字,其定义过程与定义接口非常类似,

需要注意的是:Annotation 的成员变量在 Annotation 定义中是以无参的方法形式来声明的,

其方法名和返回值类型定义了该成员变量的名字和类型,而且还可以使用 default 关键字为这个成员变量设定默认值。

package annotation.custom;
 
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface Tag {
 
    String name() default "undefined";
 
    String description();
 
}
注意:
Java 使用 Annotation 接口来代表程序元素前面的注解,该接口是所有 Annotation 类型的父接口。
自定义的注解继承了 Annotation 这个接口,因此自定义注解中包含了 Annotation 接口中所有的方法:
public interface Annotation {
 
    boolean equals(Object obj);
 
    int hashCode();
 
    String toString();
 
    Class<? extends Annotation> annotationType();
}

注解的提取

Java 在 java.lang.reflect 包下新增了 AnnotatedElement 接口,该接口代表程序中可以接受注解的程序元素,该接口主要有如下几个实现类:

AccessibleObject:是 Field、Method 和 Constructor 对象的基类。它提供了将反射的对象标记为在使用时取消默认 Java 语言访问控制检查的能力。Executable:是 Method 和 Constructor 对象的基类,提供了 Method 和 Constructor 的常用功能。Method:提供关于类或接口上单独某个方法(以及如何访问该方法)的信息。所反映的方法可能是类方法或实例方法(包括抽象方法)。Constructor:提供关于类的单个构造方法的信息以及对它的访问权限。Field:提供有关类或接口的单个字段的信息,以及对它的动态访问权限。反射的字段可能是一个类(静态)字段或实例字段。

Class:表示正在运行的 Java 应用程序中的类和接口。枚举是一种类,注释是一种接口。每个数组属于被映射为 Class 对象的一个类,所有具有相同元素类型和维数的数组都共享该 Class 对象。基本的 Java 类型(boolean、byte、char、short、int、long、float 和 double)和关键字 void 也表示为 Class 对象。

Package:包含有关 Java 包的实现和规范的版本信息。通过用于加载类的 ClassLoader 实例,可以获取并获得此版本信息。通常,此信息存储在与类一起分发的清单中。

Parameter:提供有关方法参数的信息,包括其名称和修饰符(Java8 新增)。

java.lang.reflect 包下主要包含一些实现反射功能的工具类,实际上,java.lang.reflect 包所有提供的反射 API 扩充了读取运行时 Annotation 信息的能力。当一个 Annotation 类型被定义为运行时的 Annotation 后,该注解才能是运行时可见,当 class 文件被装载时被保存在 class 文件中的 Annotation 才会被虚拟机读取。

AnnotatedElement 接口是所有程序元素(Class、Method、Constructor、Field、Parameter、Package)的父接口,所以程序通过反射获取了某个类的 AnnotatedElement 对象之后,程序就可以调用该对象的如下方法来访问 Annotation 信息

方法描述

方法描述
boolean isAnnotationPresent(Annotation annotationType)判断指定对象是否应用了某个注解,此方法主要用于方便地访问标记注释
Annotation[] getAnnotations()返回作用于指定对象的所有注解,不存在则返回长度为 0 的数组
Annotation[] getDeclaredAnnotations()返回直接作用于指定对象的所有注解,不存在则返回长度为 0 的数组(此方法忽略继承的注解)
T getAnnotation(Class annotationClass)返回指定类型的注解,不存在则返回 null
T getDeclaredAnnotation(Class annotationClass)返回直接作用于指定对象的指定类型的注解,不存在则返回 null(此方法忽略继承的注解)
T[] getAnnotationsByType(Class annotationClass)返回指定类型的注解,不存在则返回长度为 0 的数组,此方法检测其参数是否为可重复的注解类型
T[] getDeclaredAnnotationsByType(Class annotationClass)返回直接作用于指定对象的指定类型的注解,不存在则返回长度为 0 的数组,此方法检测其参数是否为可重复的注解类型(此方法忽略继承的注解)

注意: 只有当定义 Annotation 时使用了 @Retention(RetentionPolicy.RUNTIME) 修饰,

JVM 才会在装载 class 文件时提取保存在 class 文件中的 Annotation,

该 Annotation 才会在运行时可见。否则 class 文件的注解信息在执行过程中将不可用,

从而也就不能从中得到任何和注解有关的数据。

注解处理器

注解处理器是(Annotation Processor)是 javac 内置的一个用于在编译时扫描、编译和处理注解(Annotation)的工具。简单的说,在源代码编译阶段,通过注解处理器,我们可以获取源文件内注解(Annotation)相关内容。

在 Java 5 首次引入注解的时候,注解处理器的 API 还没有成熟,也没有标准化。处理注解需要一个名为 apt(Annotation Processor Tool,注解处理器工具)的独立的工具,以及包含在 com.sum.mirror 包中的 Mirror API。apt 需要使用 Mirror API 来自定义处理器。

从 Java 6 开始,注解处理器通过 JSR 269 已经标准化并被纳入到标准库中,APT 工具也被无缝集成到 Java 编译工具 javac 里面。Java 6 提供了一个已经实现通用功能的抽象类 javax.annotation.processing.AbstractProcessor,同时还提供了 javax.lang.model包。

注解处理器的用途

由于注解处理器可以在程序编译阶段工作,所以开发者可以在编译期间通过注解处理器进行开发者需要的操作。比较常用的用法就是在编译期间获取相关注解数据,然后动态生成 .java 源文件(让机器帮开发者写代码), 通常是自动产生一些有规律性的重复代码,解决了手工编写重复代码的问题,大大提升编码效率。

注解处理器的介绍

AbstractProcessor 是一个抽象类,该类实现了 Processor 接口。抽象类 AbstractProcessor 以及接口 Processor 都是位于包 javax.annotation.processing 中。 该包中定义的所有类、接口都是与实现注解处理器相关的。如下表所示:

类/接口描述
Completion某一注释的建议 completion
Filer此接口支持通过注释处理器创建新文件
Messager提供注释处理器用来报告错误消息、警告和其他通知的方式
ProcessingEnvironment注释处理工具框架将提供一个具有实现此接口的对象的注释 processor,因此 processor 可以使用该框架提供的设施来编写新文件、报告错误消息并查找其他实用工具
Processor注释 Processor 的接口
RoundEnvironment注释处理工具框架将提供一个注释处理器和一个实现此接口的对象,这样处理器可以查询有关注释处理的 round 的信息
AbstractProcessor旨在用作最具体注释 processor 的便捷超类的抽象注释 processor
Completions用来组合 Completion 对象的实用工具类
FilerException指示一个检测到试图打开某一文件的 Filer,该文件违反 Filer 提供的保证
SupportedAnnotationTypes用来指示注释处理器支持哪些注释类型的注释

自定义注解处理器

实现一个自定义注解处理器需要有两个步骤,第一是实现 Processor 接口处理注解,第二是注册注解处理器。

实现 Processor 接口

通过实现 Processor 接口可以自定义注解处理器,可以采用更简单的方法通过继承 AbstractProcessor 类实现自定义注解处理器,并实现抽象方法 process 处理想要的功能。

AbstractProcessor 具有如下方法:

方法描述
void init(ProcessingEnvironment processingEnv)用处理环境初始化 processor,方法是将 processingEnv 字段设置为 processingEnv 参数的值
boolean isInitialized()如果此对象已被初始化,则返回 true,否则返回 false
Iterable getCompletions(Element element, AnnotationMirror annotation, ExecutableElement member, String userText)返回一个空的 completion 迭代
Set getSupportedAnnotationTypes()如果 processor 类是使用 SupportedAnnotationTypes 注释的,则返回一个不可修改的集合,该集合具有与注释相同的字符串集
Set getSupportedOptions()如果 processor 类是使用 SupportedOptions 注释的,则返回一个不可修改的集合,该集合具有与注释相同的字符串集
SourceVersion getSupportedSourceVersion()如果 processor 类是使用 SupportedSourceVersion 注释的,则返回注释中的源版本
boolean process(Set annotations, RoundEnvironment roundEnv)处理先前 round 产生的类型元素上的注释类型集,并返回这些注释是否由此 Processor 声明
注意:在 Java 6 及以上版本中,getSupportedAnnotationTypes() 方法和 getSupportedSourceVersion() 方法
分别可以通过 @SupportedAnnotationTypes 注解和 @SupportedSourceVersion 注解来替换。

自定义注解处理器都需要继承于 AbstractProcessor,如下所示:

/**
 * 自定义注解处理器
 */
public class CustomProcessor extends AbstractProcessor {
 
    private Filer mFiler;
    private Messager mMessager;
    private Elements mElementUtils;
 
    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        mFiler = processingEnvironment.getFiler();
        mMessager = processingEnvironment.getMessager();
        mElementUtils = processingEnvironment.getElementUtils();
    }
 
    @Override
    public Set<String> getSupportedAnnotationTypes() {
        Set<String> annotataions = new LinkedHashSet<String>();
        annotataions.add(Tag.class.getCanonicalName());
        return annotataions;
    }
 
    @Override
    public SourceVersion getSupportedSourceVersion() {
        return SourceVersion.latestSupported();
    }
 
    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
        Set<? extends Element> tagElements = roundEnvironment.getElementsAnnotatedWith(Tag.class);
        for (Element element : tagElements) {
            // 1.获取包名
            PackageElement packageElement = mElementUtils.getPackageOf(element);
            String pkName = packageElement.getQualifiedName().toString();
            printMessage(String.format("package = %s", pkName));
            // 2.获取包装类类型
            TypeElement enclosingElement = (TypeElement) element.getEnclosingElement();
            String enclosingName = enclosingElement.getQualifiedName().toString();
            printMessage(String.format("enclosindClass = %s", enclosingName));
            // 3.获取注解的成员变量名
            String tagFiledName = element.getSimpleName().toString();
            // 4.获取注解的成员变量类型
            String tagFiledClassType = element.asType().toString();
            // 5.获取注解元数据
            Tag tag = element.getAnnotation(Tag.class);
            String name = tag.name();
            printMessage(String.format("%s %s = %s", tagFiledClassType, tagFiledName, name));
            // 6.生成文件
            createFile(enclosingElement, tagFiledClassType, tagFiledName, name);
            return true;
        }
        return false;
    }
 
    private void createFile(TypeElement enclosingElement, String tagFiledClassType, String tagFiledName, String name) {
        String pkName = mElementUtils.getPackageOf(enclosingElement).getQualifiedName().toString();
        try {
            JavaFileObject javaFileObject = mFiler.createSourceFile(pkName + ".Tag");
            Writer writer = javaFileObject.openWriter();
            writer.write(generateCode(pkName, tagFiledClassType, tagFiledName, name));
            writer.flush();
            writer.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
 
    private void printMessage(String msg) {
        mMessager.printMessage(Diagnostic.Kind.NOTE, msg);
    }
 
    private String generateCode(String pkName, String tagFiledClassType, String tagFiledName, String name) {
        StringBuilder builder = new StringBuilder();
        builder.append("package " + pkName + ";\n\n");
        builder.append("//Auto generated by apt,do not modify!!\n\n");
        builder.append("public class Tag { \n\n");
        builder.append("public static void main(String[] args){ \n");
        String info = String.format("%s %s = %s", tagFiledClassType, tagFiledName, name);
        builder.append("System.out.println(\"" + info + "\");\n");
        builder.append("}\n");
        builder.append("}");
        return builder.toString();
    }
 
}

在 init() 中可以获得如下引用:

Elements:一个用来处理 Element 的工具类。

Types:一个用来处理 TypeMirror 的工具类。

Filer:正如这个名字所示,使用 Filer 可以创建文件。

在注解处理过程中,扫描所有的 Java 源文件。源代码的每一个部分都是一个特定类型的 Element。换句话说:Element 代表程序的元素,例如包、类或者方法。每个 Element 代表一个静态的、语言级别的构件。

Element 代表的是源代码,它的子类有这些:

PackageElement:表示一个包程序元素。提供对有关包及其成员的信息的访问。

TypeElement:表示一个类或接口程序元素。提供对有关类型及其成员的信息的访问。注意,枚举类型是一种类,而注解类型是一种接口。

VariableElement:表示一个成员变量、枚举常量、方法或构造方法参数、局部变量或异常参数。

ExecutableElement:表示某个类或接口的方法、构造方法或初始化程序(静态或实例),包括注释类型元素。

TypeParameterElement:表示一般类、接口、方法或构造方法元素的形式类型参数。类型参数声明一个 TypeVariable。

注册注解处理器

注册注解处理器有两种方法:

在当前项目中的 resources/META-INF/services 目录需要新建一个特殊的文件 javax.annotation.processing.Processor,文件里的内容就是声明你的处理器。javax.annotation.processing.Processor 文件的内容是合法全称,多个处理器之间换行。

Google 提供了一个注册处理器的库,添加 com.google.auto.service:auto-service 依赖并在自定义注解处理器上添加 @AutoService(Processor.class), AutoService 注解会自动在 META-INF 文件夹下生成 javax.annotation.processing.Processor 配置信息文件,该文件里就是实现该服务接口的具体实现类。

注解的使用场景

注解是一系列元数据,它提供数据用来解释程序代码,但是注解并非是所解释的代码本身的一部分。注解对于代码的运行效果没有直接影响。 注解有许多使用场景:

1、类属性自动赋值。

2、验证对象属性完整性。

3、代替配置文件功能,像 Spring 基于注解的配置。

4、可以生成文档,像 Java 代码注释中的 @see、@param 等

5、注解有许多用处,主要如下: 提供信息给编译器:编译器可以利用注解来探测错误和警告信息 编译阶段时的处理:软件工具可以用来利用注解信息来生成代码、Html 文档或者做其它相应处理。 运行时的处理:某些注解可以在程序运行的时候接受代码的提取

6、使用 Annotation 修饰了类、方法、成员变量等之后,这些 Annotation 不会自己生效, 必须由开发者提供相应的代码来提取并处理 Annotation 信息。 这些处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。