计算机的世界是由 0 和 1 构成的,为了方便人类与计算机沟通,先贤们发明了编程语言,通过编译器将这些语言翻译成机器可以看懂的机器语言。为了方便人类更好的阅读代码,避免不必要的 996,几乎所有的编程语言都提供「注释」的特性,在某种程度上,这些「注释」的存在就是“废话”,因为编译器在执行到这里的时候是直接忽略的,「注释」虽然是人类写的,却也只是为了给“愚蠢”的人类看的。然而,无论哪个时代都有前行者,他们所做的不过是让我们的代码看起来更简洁,更有时代的进步感,因此必须要让人类与机器的沟通更进一步了,而这就是写给编译器的注释——「注解」。

何为「注解」

所谓注解,英文名 Annotation,是在 Java SE 5.0 版本中开始引入的概念,同classinterface一样,也属于一种类型。很多开发人员认为注解的地位不高,但其实不是这样的。像@Transactional、@Service、@RestController、@RequestMapping、@CrossOrigin 等等这些注解的使用频率越来越高。

说了这么多,到底何为「注解」?

注解是放在 Java 源码的类、方法、字段、参数前的一种特殊“注释”。

话不多说,直接上代码:

// 用户请求接口
@RestController
public class UserController {
    @GetMapping(value = "/user")
    public User getUser() {
        return new User("闰土",22,"男");
    }
}

上述是一个十分简单的 SpringBoot 的控制器代码,第 1 行是「注释」,是告诉阅读这行代码的人这是一个用户请求接口;第 2 行和第 4 行是「注解」,是用来编译器,这一个 Restful 接口,请求方法为 Get,匹配的是"/user"路径。

注释会被编译器直接忽略,注解则可以被编译器打包进入 class 文件,因此,注解是一种用作标注的“元数据”。从 JVM 的角度看,注解本身对代码逻辑没有任何影响,如何使用注解完全由工具决定。

「注解」的本质

在探究「注解」的本质之前,我们要先理解一个东西,Annotation 同classinterface一样,也属于一种类型,因此如果想要申明一个注解的话,我们同样需要写一个 java 文件来创建注解。注解的创建方法很简单,见下面的代码 👇

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface TestOverride {
}

如上面代码所示,创建了一个名为「TestOverride」的注解,关键词是**「@interface」**,这时有小伙伴肯定就要说了,为什么我创建一个注解前还有注解?不急,这里我们暂且按下不表,之后来解释。

回到正题,既然这是一个TestOverride.java文件,那我们就可以用 javac 命令生成字节码文件,然后再使用 Javap 命令反编译生成的字节码文件,然后一探究竟。

看到这是不是有一种恍然大悟的感觉,注解的本质就是一个继承了 Annotation 接口的接口

事实上,我们查看 JDK 源码中给人类看的「注释」也可窥探一二。

The common interface extended by all annotation types

这句话拿出来翻译一下就是:

所有注解类型所继承通用接口

回到一开始所说的额,注解是给编译器看的特殊的注释,如果没有解析它的代码,它甚至连注释都不如。

使用「注解」

解析一个类或者方法的注解往往有两种形式,一种是编译期扫描,一种是运行期反射。我们稍后讲解如何将注解与反射结合起来,先来说一下编译器扫描。

“特权”注解

所谓编译期扫描指的是编译器将 java 代码编译成字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。一般只有 JDK 内部的几个注解可以享有此特权。

@Override 为例,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。

自定义注解

通常情况下,由你自定义的注解,编译器是不知道该注解的实际作用的,因此是否将注解编译到字节码文件中,是需要由开发者自行指定注解的作用范围。而这,就要牵扯出另一个知识点了——元注解。

元注解

「元」,在汉语中有初始的意思,所谓元注解(meta annotation),即修饰注解的注解。例如上述例子中的@Target 和@Retention 注解。 JDK 已经内置了一些元注解,我们只需要使用元注解,通常不需要自己去编写元注解。「元注解」一般起到指定某个注解生命周期以及作用目标的作用。

在 JDK 中,元注解有以下四个:

  • @Target:注解的作用目标
  • @Retention:注解的生命周期
  • @Documented:注解是否应当被包含在 JavaDoc 文档中
  • @Inherited:是否允许子类继承该注解

@Documented 和@Inherited 在实际中几乎不怎么使用,这里我们仅对@Target 和@Retention 做一个详细的解释。

@Target

从名字中我们也可以看出这个注解是用来指定作用目标的,即标注你的注解到底是用来修饰方法还是修饰类的?当然,不仅仅可以修饰这两个,在源代码中用 java.lang.annotation.ElementType 的枚举常量表示,如下所示。

  • ElementType.TYPE:允许被修饰的注解作用在类、接口和枚举上
  • ElementType.FIELD:允许作用在属性字段上
  • ElementType.METHOD:允许作用在方法上
  • ElementType.PARAMETER:允许作用在方法参数上
  • ElementType.CONSTRUCTOR:允许作用在构造器上
  • ElementType.LOCAL_VARIABLE:允许作用在本地局部变量上
  • ElementType.ANNOTATION_TYPE:允许作用在注解上
  • ElementType.PACKAGE:允许作用在包上

回到@Target 注解,JDK 对其定义如下:

我们可以通过以下方式为@Target 传递作用域的值

@Target(value = {ElementType.FIELD})

@Retention

@Retention 用于指明当前注解的生命周期,和@Target 类似,需要接受一个参数用于指定相应的生命周期,同样也是一个枚举类型,用 java.lang.annotation.RetentionPolicy 的枚举常量表示,分别如下:

  • RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
  • RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
  • RetentionPolicy.RUNTIME:永久保存,可以反射获取

回到@Retention 注解,JDK 对其定义如下:

一般情况下,如果是我们自己设置的自定义注解,通常会选择使用 RetentionPolicy.RUNTIME,这样可以方便在后续的代码中通过反射获取,并为其添加一些自定义的功能。

注解与反射

提到注解,老生常谈的就是反射了,但其实我们有一点是我们需要注意的,注解的存在与反射并无关系,即使没有反射,你依然可以定义一个注解,只不过并不会对你的代码起到什么作用罢了,正因为如此,我们才需要通过反射去获取注解并提供具体的操作逻辑。

反射

反射是什么,不清楚的小伙伴可以移步阅读本篇文章 👉https://www.liaoxuefeng.com/wiki/1252599548343744/1255945147512512

这里我简单举个例子帮助你理解,Java 就是一个土豪的保险箱,你如果直接去打开保险箱土豪是不允许的,但如果你把枪顶在他头上让他开箱子,这时你就可以获得箱子里的钱了,这把枪就是「反射」,可以让你轻松获得正常情况下无法访问的内容。

处理注解

上面提到了,注解编译后其本质也是字节码文件,可以通过反射获取到,JDK 也提供了一些 API 用于解析注解,例如:

  • 通过 Class 对象的 isAnnotationPresent() 方法判断该类是否应用了某个指定的注解。
  • 通过 getAnnotation() 方法来获取注解对象。

当获取到注解对象后,就可以获取使用注解时定义的属性值。这一块的内容已经有很多人写过了,具体就是调用一下 API 的事情,这里就不多此一举了,详情可阅读 👉https://www.liaoxuefeng.com/wiki/1252599548343744/1265102026065728

下面给出示例代码:

@CrossOrigin(origins = "http://qingmiaokeji.com", allowedHeaders = "accept,content-type", methods = { RequestMethod.GET, RequestMethod.POST })
public class TestController {
	public static void main(String[] args) {
		Class c = TestController.class;

		if (c.isAnnotationPresent(CrossOrigin.class)) {
			CrossOrigin crossOrigin = (CrossOrigin) c.getAnnotation(CrossOrigin.class);

			System.out.println(Arrays.asList(crossOrigin.allowedHeaders()));
			System.out.println(Arrays.asList(crossOrigin.methods()));
			System.out.println(Arrays.asList(crossOrigin.origins()));
		}

	}
}

// 输出:[accept,content-type]
// [GET, POST]
// [http://qingmiaokeji.com]

注解与 XML

由于当下 Spring 生态已占据 Java 开发的大半江山,Spring 中的一大特点就是采用注解配置的方案逐渐替代先前 XML 配置的方案,这就势必会引发 XML 配置与注解配置之间的讨论。

  • 注解:是一种分散式的元数据,与源代码紧绑定。
  • XML:是一种集中式的元数据,与源代码无绑定。

因此注解和 XML 的选择上可以从两个角度来看:分散还是集中,源代码绑定/无绑定。

使用 XML 需要解析单独的文件,效率上比使用反射获取注解更差一些,但灵活性更强;使用注解可以降低维护成本,但是一旦需要对注解进行修改,那么要重新编译整个工程,且注解数量变多后,对于代码的简洁度有一定的影响。

于是,说了这么多,我们又回到了那个老生常谈的观点,没有最完美的技术,只有最合适的技术,不管使用注解还是 XML,做的事情还是那些事情,注解和 XML 都不是万能的,满足自己的需求且已一种更简单的方式解决掉问题即可。