Java中提供了注解(Annotation)功能,该功能可用于类、构造方法、成员变量、方法、参数等的声明中。该功能并不影响程序的运行,但是会对编译器警告等辅助工具产生影响。本文将介绍注解的概念及如何使用注解,最后将通过两个案例将所学的知识运用到实际开发中。

一、什么是注解?

1、注解的定义

注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。

所谓注解,可以看作是对 一个 类/方法 的一个扩展的模版,每个 类/方法 按照注解类中的规则,来为 类/方法 注解不同的参数,在用到的地方可以得到不同的 类/方法 中注解的各种参数与值。其实说白就是代码里的特殊标志,这些标志可以在编译,类加载,运行时被读取,并执行相应的处理,以便于其他工具补充信息或者进行部署。

2、注解的作用

注解主要有以下三种功能:

  1. 编写文档:通过代码里标识的元数据生成文档

  2. 编译检查:通过代码里标识的元数据让编译器能够实现基本的编译检查

  3. 代码分析:通过代码里标识的元数据对代码进行分析

接下来的内容将体现出注解在这三个方面的作用。

二、Java中常用的注解

1、@Override

该注解的作用是对覆盖超类中方法的方法进行标记,如果被标记的方法并没有实际覆盖超类中的方法,则编译器会发出错误警告。

下面通过举例来说明@Override注解的用法:

@Override注解的用法

除此之外,在接口的实现类中每个方法都要加上@Override注解,这里不再进行举例说明。

2、@Deprecated

该注解的作用是对不应该再使用的方法添加注解,当编程人员使用这些方法时,将会在编译时显示提示信息,它与javadoc里的@deprecated标记有相同的功能,用@Deprecated的示例代码示例如下:

@Deprecated注解的用法-1

在调用加上@Deprecated注解的方法会出现以下情况:

@Deprecated注解的用法-2

@Deprecated注解的用法-3

3、@SuppressWarnings

该注解的作用是指示编译器去忽略注解中声明的警告,其参数有:

  • deprecation:使用了过时的类或方法时的警告
  • unchecked:执行了未检查的转换时的警告
  • fallthrough:当switch程序块直接通往下一种情况而没有break时的警告
  • path:在类路径、源文件路径等中有不存在的路径时的警告
  • serial:当在可序列化的类上缺少serialVersionUID 定义时的警告
  • finally:任何finally子句不能正常完成时的警告
  • all:关于以上所有情况的警告

下面将通过一个例子来说明@SuppressWarnings注解的用法:

@SuppressWarnings注解的用法-1

如上图所示,编译器会对代码中可能出现异常或无意义的代码进行警告,如果想忽略这些警告,可以在语句、方法或类上使用@SuppressWarnings注解忽略这些警告。

@SuppressWarnings注解的用法-2

如果想要更加简单,可以直接在方法或类上添加@SuppressWarnings注解。

(1)在方法上添加@SuppressWarnings注解

@SuppressWarnings注解的用法-3

(2)在类上添加@SuppressWarnings注解

@SuppressWarnings注解的用法-4

三、注解的实现原理

既然Java自带了一些注解,那么我们可不可以自定义注解呢?答案是肯定的。不过在自定义注解之前,我们需要了解注解的实现原理,这样才能更好的理解和掌握自定义注解的方法。

1、Annotation

首先,我们查看一下Java中自带的注解是如何实现的,就以@Override注解为例,该注解的定义如下:

@Override注解定义

按照上面的格式,我们自己也来写一个注解:

1
2
3
4
package my.study.annotation.anno;

public @interface MyAnnotation {
}

之后我们用javacMyAnnotation.java进行编译,生成MyAnnotation.class字节码文件,然后再用javapMyAnnotation.class进行反编译,结果如下:

反编译结果

通过反编译我们终于发现了注解的奥秘,注解本质上就是一个接口,并且这个接口需要继承Annotation接口,接下来就去看看该接口究竟是怎样的,这里给出API文档中关于Annotation接口的说明:

API文档

通过API文档的说明,我们可以知道所有的注解接口类都要继承Annotation接口,使用@interface定义注解时即表示继承了 java.lang.annotation.Annotation 接口。

注意:注解的实现和我们通常实现接口的方法不同。Annotation 接口的实现细节都由编译器完成。通过 @interface定义注解后,该注解不能继承其他的注解或接口。

下面给出Annotation的整体结构图:

Annotation的整体结构图

从图中可以看出对于每一个Annotation对象都有唯一的RetentionPolicy属性,对于每一个Annotation可以有若干个ElementType属性。此外Annotation有很多的子类,包括TargetRetention等,其中有一部分是元注解,关于元注解的概念下文将会详细介绍,接下来将要详细介绍的是ElementType属性和RetentionPolicy属性。

2、ElementType

ElementType是枚举类型,用来指定Annotation的类型,该枚举类型包含以下成员:

成员 说明
TYPE 类、接口(包括注释类型)或枚举声明
FIELD 字段声明(包括枚举常量)
METHOD 方法声明
PARAMETER 参数声明
CONSTRUCTOR 构造方法声明
LOCAL_VARIABLE 局部变量声明
ANNOTATION_TYPE 注释类型声明
PACKAGE 包声明
TYPE_PARAMETER 类型参数声明(自JDK1.8起支持)
TYPE_USE 类型的使用(自JDK1.8起支持)

3、RetentionPolicy

RetentionPolicy是枚举类型,用来指定Annotation的策略,该枚举类型包含以下成员:

成员 说明
SOURCE Annotation信息仅存在于编译器处理期间,编译完成即被删除
CLASS 编译器将Annotation存储于类对应的.class文件中,这是默认行为
RUNTIME 编译器将Annotation存储于class文件中,并且可由JVM读入
  • 若注解的类型为SOURCE,则意味着:注解仅存在于编译器处理期间,编译器处理完之后,该注解就没用了。 例如,@Override标志就是一个注解。当它修饰一个方法的时候,就意味着该方法覆盖父类的方法;并且在编译期间会进行语法检查!编译器处理完后,@Override就没有任何作用了。
  • 若注解的类型为CLASS,则意味着:编译器将注解存储于类对应的.class 文件中,它是注解的默认行为。
  • 若注解的类型为RUNTIME,则意味着:编译器将注解存储于.class文件中,并且可由JVM读入。

三、元注解

什么是元注解呢?通俗地讲就是用于描述注解的注解。Java内部已经定义了一些元注解,下面通过表格介绍一些常用的元注解:

注解名称 作用
@Target 描述注解能够作用的位置,需要传入ElementType类型的值
@Retention 描述注解被保留的阶段,需要传入RetentionPolicy类型的值
@Documented 描述注解是否被抽取到API文档中
@Inherited 描述注解是否被子类继承

若需获取更多关于注解的信息,请参阅:java.lang.annotation

四、自定义注解

在介绍完注解的实现原理以及元注解的相关概念之后,我们就可以自定义注解了,下面将详细介绍如何自定义注解。

1、注解的格式

定义注解的格式为:

1
2
3
4
@元注解
public @interface 注解名称 {
// 属性列表
}

其中元注解的含义上文已经介绍过了,这里不再赘述。属性实际上就是接口中的抽象方法。下面将举一个简单的例子来说明如何定义一个注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package my.study.annotation.anno;

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

// 元注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {

// 属性
String value();

// 属性
int num();
}

2、注解的属性定义

关于注解中的属性,有以下几点要求:

(1)属性的返回值类型必须为以下几种类型

  • 基本数据类型
  • String
  • 枚举
  • 注解
  • 以上类型的数组

(2)定义了属性,在使用时需要给属性赋值

元注解-1

元注解-2

注:如果定义属性值时使用default关键字给属性默认初始化值,则使用注解时可以不进行赋值。此外,如果只有一个属性需要赋值,且需要赋值的属性名称为value,则使用注解时可直接传入value的属性值,value属性名可省略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package my.study.annotation.anno;

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

// 元注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyAnnotation {

// value属性
String value();

// num属性,默认为1
int num() default 1;
}

元注解-3

(3)属性返回值类型为数组的注解在赋值时传入的值用{}包裹,如果数组中只有一个值,则{}省略

1
2
3
4
// 在传入多个值时用{}包裹
@Target({ElementType.METHOD, ElementType.FIELD})
// 当只有一个值时{}省略
@Target(ElementType.METHOD)

五、案例1:实现一个简单的测试框架

1、需求

如果我们需要实现一个简单的测试框架,该框架在运行之后会自行检测所有加上@Check注解的方法,并判断该方法是否有异常,而且要将检测的结果记录到文件中。下面给出@Check注解和被检测的Calculator类的定义:

(1)@Check注解

1
2
3
4
5
6
7
8
9
10
11
12
package my.study.annotation.anno;

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

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Check {
}

(2)Calculator

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package my.study.annotation.pojo;

import my.study.annotation.anno.Check;

/**
* 计算器类
*/
public class Calculator {

// 加法
@Check
public void add () {
System.out.println("1 + 0 = " + (1 + 0));
}

// 减法
@Check
public void sub () {
System.out.println("1 - 0 = " + (1 - 0));
}

// 乘法
@Check
public void mul () {
System.out.println("1 * 0 = " + (1 * 0));
}

// 除法
@Check
public void div () {
System.out.println("1 / 0 = " + (1 / 0));
}
}

2、分析

要想实现该功能,需要利用Java中的反射机制。首先查找Calculator类中所有加上@Check注解的方法,之后执行该方法,对可能出现的异常进行捕获,并记录到文件中。

3、实现

完整的功能实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
package my.study.annotation;

import my.study.annotation.anno.Check;
import my.study.annotation.pojo.Calculator;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;

/**
* 实现一个简单的测试框架
* 该框架在运行之后会自行检测所有加上`@Check`注解的方法,并判断该方法是否有异常
* 并且要将检测的结果记录到文件中
*/
public class TestDemo {
public static void main(String[] args) throws IOException {
// 1.创建计算器对象
Calculator calculator = new Calculator();
// 2.获取字节码文件对象
Class calculatorClass = calculator.getClass();
// 3.获取所有方法
Method[] methods = calculatorClass.getMethods();
int count = 0; // 出现异常的次数
BufferedWriter bw = new BufferedWriter(new FileWriter("bug.txt"));

for (Method method : methods) {
// 4.判断方法上是否有Check注解
if (method.isAnnotationPresent(Check.class)) {
// 5.有。,执行该方法
try {
method.invoke(calculator);
} catch (Exception e) {
// 6.捕获异常
count++;
// 7.记录到文件中
bw.write(method.getName() + "方法出现异常");
bw.newLine();
bw.write("异常的名称: " + e.getCause().getClass().getSimpleName());
bw.newLine();
bw.write("异常的原因: " + e.getCause().getMessage());
bw.newLine();
bw.write("-------------------------------");
bw.newLine();
}
}
}
bw.write("本次测试一共出现" + count + "次异常");
bw.flush();
bw.close();
}
}

运行程序,bug.txt中的内容如下:

1
2
3
4
5
div方法出现异常
异常的名称: ArithmeticException
异常的原因: / by zero
-------------------------------
本次测试一共出现1次异常

六、案例2:利用注解代替配置文件改写框架

1、需求

在之前一篇关于Java反射机制的文章中,我们在文章的最后提到了一个案例,该案例要求写一个“框架”,可以创建任意类的对象并执行其中任意方法。在那篇文章中我们利用反射和配置文件来实现了“框架”,其实可以用注解来代替配置文件,同样可以实现该功能。那么该如何去写呢?

2、分析

可以编写一个注解,该注解包含classNamemethodName两个属性,在功能实现类上加上该注解,并传入属性值。这样就可以利用反射机制获取到功能实现类的注解,进而获取到传入的属性值。后面的步骤就与之前的实现方法相同,这里不再赘述。

3、实现

(1)@Properties注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package my.study.annotation.anno;

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Properties {
// 要创建的全类名
String className();
// 要执行的方法名
String methodName();
}

(2)功能实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package my.study.annotation;

import my.study.annotation.anno.Properties;

import java.lang.reflect.Method;

@Properties(className = "my.study.annotation.pojo.Person", methodName = "eat")
public class FrameworkDemo {
public static void main(String[] args) throws Exception {
// 1.获取该类的字节码文件对象
Class<FrameworkDemo> frameworkDemoClass = FrameworkDemo.class;
// 2.获取@Properties注解
// 这一步本质上就是在内存中生成了一个该注解接口的子类实现对象
Properties properties = frameworkDemoClass.getAnnotation(Properties.class);
// 3.获取注解的属性值
String className = properties.className();
String methodName = properties.methodName();
// 4.加载该类进内存
Class cls = Class.forName(className);
// 5.创建对象
Object object = cls.newInstance();
// 6.获取方法对象
Method method = cls.getMethod(methodName);
// 7.执行方法
method.invoke(object);
}
}

运行程序,结果与之前的结果相同,这里不再赘述。

七、总结

注解本质上就是用来描述类、方法或者成员变量的代码级说明,在实际运用中常常与反射机制结合在一起使用。使用注解可以摆脱框架中复杂的配置文件,使开发人员可以专注于业务逻辑的实现。因此掌握注解相关的知识是非常重要的。