注解,这个经常在开发中使用到的东西,它的使用语法是怎么样的?如何去自定义一个注解呢?

什么是注解

我们在日常开发中,比如 java 中的@Override,在 springboot 中用到的@SpringBootApplication等一系列标注在类或者方法上的注解。我们添加上注解后会有对应的事件处理,比如我们的@Override注解标明这个方法是重写了父类或者接口的方法,当参数不一致、返回类型不一致等不符合重写的要求时,编译器会报错。类似的@SpringBootApplication也是标明这个项目的一个 springboot 项目,默认会启动一个 tomcat 容器等。

注解是从 jdk5 开始引入的新特性。

注解的语法

1
public @interface FirstAnnotation {}

通过@interface即可声明一个注解。

内置的注解

Java 定义了一套注解,共有 7 个,3 个在 java.lang 中,剩下 4 个在 java.lang.annotation 中

作用在代码的注解是

  • @Override - 检查该方法是否是重载方法。如果发现其父类,或者是引用的接口中并没有该方法时,会报编译错误。
  • @Deprecated - 标记过时方法。如果使用该方法,会报编译警告。
  • @SuppressWarnings - 指示编译器去忽略注解中声明的警告。

元注解

我们通过上面的语法定义了一个注解,但还需要其他的注解一同作用。在 jdk1.5中定义了 4 个标准注解,它们用来提供对其他 annotation 类型做说明。分别是:

  • @Retention - 标识这个注解怎么保存,是只在代码中,还是编入class文件中,或者是在运行时可以通过反射访问。
  • @Documented - 标记这些注解是否包含在用户文档中。
  • @Target - 标记这个注解应该是哪种 Java 成员。
  • @Inherited - 标记这个注解是继承于哪个注解类(默认 注解并没有继承于任何子类)

从 Java 7 开始,额外添加了 3 个注解:

  • @SafeVarargs - Java 7 开始支持,忽略任何使用参数为泛型变量的方法或构造函数调用产生的警告。
  • @FunctionalInterface - Java 8 开始支持,标识一个匿名函数或函数式接口。
  • @Repeatable - Java 8 开始支持,标识某注解可以在同一个声明上使用多次。

@Retention

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Retention {

RetentionPolicy value();
}


public enum RetentionPolicy {
/* 注解被编译器丢弃,不会保留到 class 文件中 */
SOURCE,
/* 默认的类型。 注解在 class 文件中可用,但会被 jvm 丢弃 */
CLASS,
/*在运行期也保留,可以通过反射机制读取注解的信息*/
RUNTIME
}

这个注解只有一个变量,类型是一个枚举。可以通过这个变量来表明这个注解是保存在源码中(source),还是编入 class 文件中(class),还是在运行时通过反射访问(runtime)。

@Target

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
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Target {

ElementType[] value();
}


public enum ElementType {
/* 表明该注解可以用于 类,接口(包括注解类型)或枚举声明*/
TYPE,
/* 表明该注解可以用于 字段声明(包括枚举常量) */
FIELD,
/* 表明该注解可以用于 方法声明 */
METHOD,
/* 表明该注解可以用于 参数声明 */
PARAMETER,
/* 表明该注解可以用于 构造函数声明 */
CONSTRUCTOR,
/* 表明该注解可以用于 局部变量声明 */
LOCAL_VARIABLE,
/* 表明该注解可以用于 注解声明(应用于另一个注解上) */
ANNOTATION_TYPE,
/* 表明该注解可以用于 包声明 */
PACKAGE,
/* 表明该注解可以用于 类型参数声明@since 1.8 */
TYPE_PARAMETER,
/* 类型使用声明 */
TYPE_USE
}

@Documented

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Documented {
}

它没有任何变量,添加这个注解可以让我们执行 JavaDoc 文档打包时注解会被保存进 doc 文档,反之将在打包时丢弃。

@Inherited

1
2
3
4
5
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.ANNOTATION_TYPE)
public @interface Inherited {
}

这个注解是什么意思呢?我们通过一个例子来说明一下:

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
@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentFirst {
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface DocumentSecond {
}

@DocumentFirst
class A{ }

class B extends A{ }

@DocumentSecond
class C{ }

class D extends C{ }


public class DocumentDemo {

public static void main(String[] args){
A instanceA=new B();
System.out.println("实例 B 拥有的注解:"+Arrays.toString(instanceA.getClass().getAnnotations()));

C instanceC = new D();

System.out.println("实例 D 拥有的注解:"+Arrays.toString(instanceC.getClass().getAnnotations()));
}

/**
* 运行结果:
实例 B 拥有的注解:[@cn.lnj.annotationdemo.DocumentFirst()]
实例 D 拥有的注解:[]
*/
}

可以看出来,添加了@Inherited元注解的注解,这个类被继承后的子类也可以拥有这个注解。也就是说这个注解的作用范围是父类和子类。而没有添加这个元注解的注解,作用范围只有被添加的类。

@Repeatable

添加这个注解后,注解可以在同一个声明上使用多次,来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public @interface DocumentA {
String[] names;
}

@Repeatable
public @interface DocumentB {
String[] names;
}

//相同的注解,不能同时声明多次,因为没有添加@Repeatable注解
@DocumentA(names="AAA")
//@DocumentA(names="BBB")
class A { }

//相同的注解,可以同时被声明多次,不会报错
@DocumentB(names="AAA")
@DocumentB(names="BBB")
class B { }

@FunctionalInterface

在 jdk1.8中添加了 lambda 表达式,也就是函数式编程。虽然说添加了lambda,但也并不是所有的东西都可以无条件的去使用。需要在接口上添加@FunctionalInterface注解,并且这个接口里面只能有一个抽象方法。

通过 aop 来进行拦截

通过上面我们可以进行一个自定义的注解,但是我们即便把他声明在方法上,它其实还是没有作用的,如果需要这个注解起到我们想要的作用还需要进行自定义的处理。比如我们使用 aop 来进行这个处理

首先定义一个注解

1
2
3
4
5
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface HelloAnnotation {

}

然后在方法上添加这个注解

1
2
3
4
@HelloAnnotation
public void sayHello(){
System.out.println("i am working");
}

最后定义拦截器

1
2
3
4
5
6
7
8
9
@Around("@annotation(HelloAnnotation)")
public Object around(ProceedingJoinPoint proceedingJoinPoint, HelloAnnotation helloAnnotation) {
//方法执行前
System.out.println("start");
//调用方法
proceedingJoinPoint.proceed();
//方法执行后
System.out.println("finish");
}

这样我们就可以在调用sayHello()这个方法后,执行这个拦截器逻辑。

参考