注解是 Java1.5 之后引入的一个特性,在日常的开发中,我们或多或少的都能涉及到跟注解有关的操作。
那么,在享受注解带给我们的诸多便利的背后,它究竟是通过什么方式在程序中运行的呢?
本篇文章将带领大家一起了解一下注解的相关知识。
下图是本篇文章涉及全部内容的思维导图。
1. 什么是注解
1.1 元数据
在正式介绍注解之前,我们有必要先来了解一个基础知识,元数据。
百度百科中,对于元数据的介绍如下:
元数据(Metadata)是描述其它数据的数据(data about other data),或者说是用于提供某种资源的有关信息的结构数据(structured data)。元数据是描述信息资源或数据等对象的数据,其使用目的在于:识别资源;评价资源;追踪资源在使用过程中的变化;实现简单高效地管理大量网络化数据;实现信息资源的有效发现、查找、一体化组织和对使用资源的有效管理。
为了方便理解,接下来我们用一个 java 代码中常见的例子来做进一步解释。
@override
在大家日常编程过程中,一定会经常见到 @override 这个注解,只要我们在覆写父类方法的地方加上它,IDE 就会在编码过程中替我们检测覆写的正确性。
显然,@override 在代码执行的逻辑中并没有任何的实际意义,它存在唯一的作用就是标记源码中某个方法是覆写于父类的,换句话说,是用来描述源码本身的某种属性的一种源码。而这种用来描述源码的源码,或者说,描述数据的数据,也就是我们先前所提到的,元数据。
讲到这,大家大概已经发现了, @override 在 java 中是一个典型的内置注解,而我用它来解释元数据的定义,是因为,所谓注解,其实就是一种元数据。
1.2 注解
上文提到,注解就是一种元数据。
在 java 中,它的作用就是用来描述代码本身的某些属性,而作为信息载体的它,与业务逻辑本身并无关系。
在源码内,注解会以一种特殊修饰符的形式应用于类、方法、参数、变量、构造器及包声明中,以 @加上注解名称并以括号接收参数的形式调用。
@MyAnnotation(Param1,Param2... ...)
众所周知,在 Java 中,万物皆对象,而注解也不例外。实际上,注解在定义时与接口的定义极为相似,也可认为,注解,就是一个继承自 java.lang.annotation.Annotation 接口的特殊接口,这一点,在本文稍后的语法部分将进行更详细的介绍。
那么,既然注解是一种接口,提到在程序中直接调用接口,我们自然而然地就会联想到 Java 中匿名类的实现。
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("这是一个匿名类,他的实现其实就是创建了一个接口的子类。");
}
});
在 Java 中,用接口定义匿名类看起来是在实例化一个接口,实际上是通过匿名类的机制实现了一个类,并且动态创建这个匿名类的实例。
类似的,在解析注解的时候,Java使用了动态代理对我们定义的注解接口生成了一个代理类,而对注解的属性设置其实都是在对这个代理类中的变量进行赋值。
2. 注解的用处
2.1 注解 vs 配置文件
在注解出现之前,描述元数据的功能通常是通过 XML 等以配置文件的形式实现的。那么,我们为什么又要引入一个新的概念来实现一个已经实现的功能呢?
其实,之所以要引入注解,主要还是因为配置文件的维护过于繁琐。
有时候,人们在进行代码描述时,会更希望使用一些和代码紧耦合的方式,而不是像配置文件那样和代码完全分离。这种需求显然是配置文件无法实现的,因为最初人们引入配置文件,其目的就是为了分离代码和配置。而注解本身几乎可以存在于源码的任何位置,这种便利性使得注解在一些情况下比配置文件更加适用。
那么,在日常开发中,对于这两种从设计初衷来看似乎完全相反的功能,我们应该如何去选择呢?
在 stackoverflow上,人们已经对这两者的利弊进行了许多讨论 : Xml configuration versus Annotation based configuration
举个简单的例子,假如我们的目的是为应用设置很多的常量或参数,那么最好用配置文件的方式,因为它不会同特定的代码相连。而如果目的是想把某个方法声明为服务,那么使用注解会更好一些,因为这种情况下需要注解和方法紧密耦合起来。
2.2 标准的描述方式
另外,引入注解还有一个很重要的原因是,它定义了一种标准的描述元数据的方式。在这之前,开发人员通常使用他们自己的方式定义元数据。例如,使用标记接口,注释等等。每个人都按照自己的方式定义元数据,而非像注解这种统一的标准。
目前,许多框架都会将配置和注解两种方式结合使用,平衡两者之间的利弊。
3. 相关语法
如果想要在程序中自定义一个注解,需要编写的代码结构如下:
//元注解
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
//注解的定义
public @interface MyAnnotation {
enum MyEnum {
GREEN, YELLOW, RED
}
//可用数据类型
String value() default "value";
int number();
MyEnum color() default MyEnum.GREEN;
}
在这段代码中,我们需要关注的有三点 :
- 元注解
- 注解的定义
- 注解中可以使用的数据类型
3.1 元注解
前文中,我们介绍过元数据的概念,而元注解与元数据一样,是一种用来描述注解的注解。
在 Java 中,内置元注解总共有四种:@Retention,@Target,@Document,@Inherited
-
@Retention :该注解定义了被修饰注解的生命周期,在 RetentionPolicy 中定义了常量作为该注解的 value 参数。
常量名 意义 RetentionPolicy.SOURCE 在编译阶段丢弃。这些注解在编译结束之后就不再有任何意义,所以它们不会写入字节码。 RetentionPolicy.CLASS 在类加载的时候丢弃。在字节码文件的处理中有用。 RetentionPolicy.RUNTIME 始终不会丢弃,运行期也保留该注解,因此可以使用反射机制读取该注解的信息。 -
@Target :定义注解可以作用的目标,在 ElementType 中定义了常量作为该注解的 value 参数。
常量名 意义 ElementType.TYPE 表示可以用来注解类、接口、注解类型或枚举类型 ElementType.FIELD 可以用来注解属性(包括枚举常量) ElementType.METHOD 可以用来注解方法 ElementType.PARAMETER 可以用来注解参数 ElementType.CONSTRUCTOR 可以用来注解构造器 ElementType.LOCAL_VARIABLE 可用来注解局部变量 ElementType.ANNOTATION_TYPE 可以用来注解 注解类型 ElementType.PACKAGE 可以用来注解包 @Documented :表示是否将注解信息添加在java文档中
@Inherited :表示该注解会被子类继承( 仅针对类,成员属性、方法并不受此注释的影响 )
3.2 注解的定义
跟定义接口一样,但是在 interface 前需要接一个 @ 字符标明是注解。( @ 和 interface 是两个不同的关键字,中间可以有空格间隔,但是为了保持语义,通常连起来使用)
public @interface MyAnnotation
3.3 可用数据类型
注解只支持基本类型、String及枚举类型。注释中所有的属性被定义成方法,并允许提供默认值。
String value() default "value";
int number();
MyEnum color() default MyEnum.GREEN;
4. 自定义注解 DEMO
4.1 概述
DEMO 实现的功能为模拟 SpringBoot 自动配置(幼儿园简化版):
如果内存中存在 Worker 实例,则调用该实例。
如果不存在实例,并且自动配置开关开启,则自动创建一个 worker 实例并调用。
4.2 源码实现
- worker ,为了分辨自动创建与手动创建,所以创建一个 worker 接口,并定义两个不同的实现
//统一接口
public interface Worker {
public void work();
}
//自动创建的实例
public class AutoWorker implements Worker{
@Override
public void work() {
System.out.println("AutoWorker");
}
}
//手动创建的实例
public class ManualWorker implements Worker{
@Override
public void work() {
System.out.println("ManualWorker");
}
}
- 自定义注解,拥有一个布尔变量用来记录自动配置开关状态
//注解的生命周期为 runtime,永远不会被丢弃,通常自定义注解都使用这个生命周期
@Retention(RetentionPolicy.RUNTIME)
//将该注解添加到 java 文档中
@Documented
//该注解可以标注类、接口、注解类型或枚举类型上
//如果注解中的属性为数组,则用{}表示数组,并用逗号分隔,如果只有一个值也可以省略{}
@Target({ElementType.TYPE})
public @interface AutoiConfigAnnotation {
//想要配置为自动配置的类型
public String className();
//自动配置开关
boolean autoConfigSwitch() default true;
}
- 创建一个用来加载注解的类
@AutoiConfigAnnotation(className="Worker",autoConfigSwitch = true)
public class BasicClass {
public BasicClass(){
System.out.println("BasicClass is created");
}
}
- 执行逻辑的 Main 函数
public class Main {
public static Worker worker = null;
public static void main(String[] args) {
//加载注解
BasicClass bc = new BasicClass();
worker = new ManualWorker(); //手动创建
//根据注解处理自动配置逻辑
processAnnotation(bc);
worker.work();
}
public static void processAnnotation(BasicClass bc) {
//通过被标注对象的 getClass().getAnnotation()获取注解实例
AutoiConfigAnnotation autoiConfigAnnotation = bc.getClass().getAnnotation(AutoiConfigAnnotation.class);
if (autoiConfigAnnotation != null) {
//检查 autoConfigSwitch 属性
if (autoiConfigAnnotation.autoConfigSwitch()) {
//获取 className 属性
String className = autoiConfigAnnotation.className();
try {
Class clazz = Class.forName(className);
//如果内存中已经存在 worker 或者想要配置为自动配置的类型不是 worker, return
if (worker != null || clazz.isInstance(worker)) {
return;
} else {
worker = new AutoWorker();
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
return;
}
}
}
}
- 执行结果
通过修改注解的属性与是否手动创建对象可以测试出不同情况下的结果:
5. 参考资料
注解的那点事儿
Java中的注解是如何工作的?
[Java基础加强总结(一)——注解(Annotation)]
完整思维导图: