注解是放在Java源码的类、方法、字段、参数前的一种特殊“注释”,是Java语言用于工具处理的标注,比如常用的@Override

Java的注解可以分为三类:

  • 第一类是由编译器使用的注解,例如:

    • @Override:让编译器检查该方法是否正确地实现了覆写;
    • @SuppressWarnings:告诉编译器忽略此处代码产生的警告。

    这类注解不会被编译进入.class文件,它们在编译后就被编译器扔掉了。

  • 第二类是由工具处理.class文件使用的注解,比如有些工具会在加载class的时候,对class做动态修改,实现一些特殊的功能。这类注解会被编译进入.class文件,但加载结束后并不会存在于内存中。这类注解只被一些底层库使用,一般我们不必自己处理。

  • 第三类是在程序运行期能够读取的注解,它们在加载后一直存在于JVM中,这也是最常用的注解。例如,一个配置了@PostConstruct的方法会在调用构造方法后自动被调用(这是Java代码读取该注解实现的功能,JVM并不会识别该注解)。

注解可以配置参数,没有指定配置的参数使用默认值;如果参数名称是value,且只有一个参数,那么可以省略参数名称。

定义注解

  1. @interface语法来定义注解(Annotation)。
  2. 添加参数、默认值,把最常用的参数定义为value()
  3. 用元注解配置注解,必须设置@Target@Retention@Retention一般设置为RUNTIME
1
2
3
4
5
6
7
8
9
10
11
@Target({
ElementType.TYPE,
ElementType.METHOD,
ElementType.FIELD
})
@Retention(RetentionPolicy.RUNTIME)
public @interface Report {
int type() default 0;
String level() default "info";
String value() default "";
}

元注解

有一些注解可以修饰其他注解,这些注解就称为元注解

@Target

使用@Target可以定义Annotation能够被应用于源码的哪些位置:

  • 类或接口:ElementType.TYPE
  • 字段:ElementType.FIELD

@Retention

定义了Annotation的生命周期:

  • 仅编译期:RetentionPolicy.SOURCE
  • 仅class文件:RetentionPolicy.CLASS
  • 运行期:RetentionPolicy.RUNTIME

@Repeatable

使用@Repeatable这个元注解可以定义Annotation是否可重复。

自定义注解经过@Repeatable修饰后,在某个类型声明处,就可以添加多个此注解

@Inherited

使用@Inherited定义子类是否可继承父类定义的Annotation@Inherited仅针对@Target(ElementType.TYPE)类型的annotation有效,并且仅针对class的继承,对interface的继承无效

使用注解

SOURCE类型的注解主要由编译器使用,因此我们一般只使用,不编写。CLASS类型的注解主要由底层工具库使用,涉及到class的加载,一般我们很少用到。只有RUNTIME类型的注解不但要使用,还经常需要编写。

读取注解,需要使用反射API:

1
2
3
4
5
6
7
8
9
10
11
Class cls = Person.class;
// 判断@Report是否存在于Person类
if (cls.isAnnotationPresent(Report.class)) {
Report report = cls.getAnnotation(Report.class);
...
}
// or
Report report = cls.getAnnotation(Report.class);
if (report != null) {
...
}

读取方法参数的Annotation需要多考虑一些,方法参数本身可以看成一个数组,而每个参数又可以定义多个注解,一次获取方法参数的所有注解就必须用一个二维数组来表示,例如下述方法:

1
2
public void hello(@NotNull @Range(max=5) String name, @NotNull String prefix) {
}

对应的获取:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取Method实例:
Method m = ...
// 获取所有参数的Annotation:
Annotation[][] annos = m.getParameterAnnotations();
// 第一个参数(索引为0)的所有Annotation:
Annotation[] annosOfName = annos[0];
for (Annotation anno : annosOfName) {
if (anno instanceof Range) { // @Range注解
Range r = (Range) anno;
}
if (anno instanceof NotNull) { // @NotNull注解
NotNull n = (NotNull) anno;
}
}

定义了注解,本身对程序逻辑没有任何影响。我们必须自己编写代码来使用注解。

例:一个Person实例Filed字段的检查方法,检查其是否满足@Range的定义。如果字段类型是String,就检查String的长度,如果字段是int,就检查int的范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static void check(Person person) throws IllegalArgumentException, ReflectiveOperationException {
for (Field field : person.getClass().getFields()) {
Range range = field.getAnnotation(Range.class);
if (range != null) {
Object value = field.get(person);
if (value instanceof String) {
String temp = (String) value;
if (temp.length() < range.min() || temp.length() > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
} else if (value instanceof Integer) {
Integer temp = (Integer) value;
if (temp < range.min() || temp > range.max()) {
throw new IllegalArgumentException("Invalid field: " + field.getName());
}
}
}
}
}

Person类:

1
2
3
4
5
6
7
8
public class Person {

@Range(min = 1, max = 20)
public String name;

@Range(min = 1, max = 100)
public int age;
...

Range注解:

1
2
3
4
5
6
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Range {
int min() default 0;
int max() default 255;
}

这样一来,我们通过@Range注解,配合check()方法,就可以完成Person实例的检查。相比于在构造方法里写n个判断,用注解更加直观,只需要在每个成员前加上注解,然后写一个检查方法就可以检查所有的成员是否满足条件。