一、概述
关于注解,首先引入官方文档的一句话:Java 注解用于为 Java 代码提供元数据。作为元数据,注解不直接影响你的代码执行,但也有一些类型的注解实际上可以用于这一目的。接下我将从注解的定义、元注解、注解属性、自定义注解、注解解析JDK 提供的注解这几个方面再次了解注解(Annotation)。
- 注解就像是一种标记;
- 可以作用在类的源码时期,编译时期和运行时期;
- 标记后的类,可以基于反射或字节码注入的形式对其实施一些操作;
- 注解是一种元编程的概念;
二、注解的语法
1、定义注解
日常开发中,我们使用 class、interface 比较多,而注解和它们一样,也算一种类的类型,使用的修饰符为 @interface
新建一个注解 NotNull,如下:
public @interface NotNull { }
接着我们可以使用 NotNull 注解作用在类、方法和参数上
@NotNull public class App { @NotNull public static void main(@NotNull String[] args) { } }
以上,我们只是了解的注解的写法,但是我们定义的注解中没有任何代码,这个注解暂时没有任何意义。那么,要如何使注解工作呢?我们下面来了解元注解。
2、元注解
元注解我们可以理解为注解的注解,它是用来修饰其他注解的,方便我们使用注解来实现我们想要的功能。
元注解分别有以下五种:
@Retention
- Retention英文意思有保留、保持的意思。在@Retention注解中使用枚举 RetentionPolicy 来表示注解保留时期,枚举的取值分别为:SOURCE、CLASS和RUNTIME,分别代表源码期、class字节码文件期和运行期。
- @Retention(RetentionPolicy.SOURCE):注解仅存在于源码中,当Java文件编译成class文件的时候,注解被遗弃;
- @Retention(RetentionPolicy.CLASS): 默认的保留策略,注解会在class字节码文件中存在,但运行时被遗弃;
- @Retention(RetentionPolicy.RUNTIME): 注解不仅会在class字节码文件中存在,在运行时可以通过反射获取到;
我们自定义的注解如果只存在于源码期或class字节码期就无法发挥作用,而在运行期能够获取到注解才是我们最终的目的,所以,自定义注解一般是使用@Retention(RetentionPolicy.RUNTIME),如下:
@Retention(RetentionPolicy.RUNTIME) public @interface NotNull { }
@Target
- @Target 英文意思是目标,用来描述修饰的对象范围,可以包括: packages、types(类、接口、枚举、Annotation类型)、类型成员(方法、构造方法、成员变量、枚举值)、方法参数和本地变量(如循环变量、catch参数);
- @Target(ElementType.TYPE) :作用接口、类、枚举、注解;
- @Target(ElementType.FIELD) :作用属性字段、枚举的常量;
- @Target(ElementType.METHOD) :作用方法;
- @Target(ElementType.PARAMETER) :作用方法参数;
- @Target(ElementType.CONSTRUCTOR) :作用构造方法;
- @Target(ElementType.LOCAL_VARIABLE):作用局部变量;
- @Target(ElementType.ANNOTATION_TYPE):作用于注解(@Retention注解中就使用该属性);
- @Target(ElementType.PACKAGE): 作用于包;
- @Target(ElementType.TYPE_PARAMETER) :作用于类型泛型,即泛型方法、泛型类、泛型接口 (jdk1.8加入);
- @Target(ElementType.TYPE_USE) :类型使用,可以用于标注任意类型除了 class (jdk1.8加入);
比较常用的的是 ElementType.TYPE 类型,我们来看一下:
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.PARAMETER}) public @interface NotNull { }
注解 NotNull 在定义时 Target 声明了只能作用于方法和方法参数,所以在使用时,作用于类时就报错了。
@Document
- Document:它的作用是标识注解写进 javadoc 文档;
@Inherited
- Inherited的英文意思是继承,这个继承和我们平时理解的继承大同小异,一个被 @Inherited 注解了的注解修饰了一个父类,如果他的子类没有被其他注解修饰,则它的子类也继承了父类的注解;

@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.TYPE}) public @interface NotNull { }

@NotNull public class Father { }

public class Son extends Father { }

public class App { public static void main(String[] args) { //获取Son的class对象 Class<Son> sonClass = Son.class; // 获取Son类上的注解NotNull可以执行成功 NotNull annotation = sonClass.getAnnotation(NotNull.class); // 打印子类注解名称 System.out.println(annotation); } }
运行结果将子类 Son 的注解获取到并打印出来,说明子类 Son 继承了父类 Father 的注解 NotNull,是因为在定义 NotNull 注解时加上了元注解 @Inherited。
@Repeatable(JDK1.8加入)
- Repeatable的英文意思是可重复的。顾名思义说明被这个元注解修饰的注解可以同时作用一个对象多次,但是每次作用注解又可以代表不同的含义。

/** * 一个人喜欢玩游戏,他喜欢玩英雄联盟,绝地求生,极品飞车,尘埃4等 * 则我们需要定义一个People的注解,他属性代表喜欢玩游戏集合 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface People { Game[] value(); }

/** * 游戏注解,游戏属性代表游戏名称 */ @Repeatable(People.class) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Game { String value() default "王者"; }

/** * 玩游戏类 */ @Game(value = "LOL") @Game(value = "PUBG") @Game(value = "NFS") @Game(value = "Dirt4") public class PlayGame { }
通过上面的例子,解释了 @Repeatable 注解的用法。但是大家可能会有疑问,游戏注解中括号的变量是什么?其实这和游戏注解中定义的属性对应。
接下来我们继续学习注解的属性。
3、注解的属性
注解的属性其实和类中定义的属性有异曲同工之处,只是注解中的属性都是成员属性,并且注解中是没有方法的,只有成员属性,属性名就是使用注解括号中对应的参数名,变量返回值注解括号中对应参数类型。相信这会你应该会对上面的例子有一个更深的认识。而@Repeatable注解中的变量则类型则是对应Annotation(接口)的泛型Class。

@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) public @interface Repeatable { /** * Indicates the <em>containing annotation type</em> for the * repeatable annotation type. * @return the containing annotation type */ Class<? extends Annotation> value(); }

public interface Annotation { boolean equals(Object obj); int hashCode(); String toString(); Class<? extends Annotation> annotationType(); }
通过以上源码,我们知道注解本身就是Annotation接口的子接口,也就是说注解中其实是可以有属性和方法,但是接口中的属性都是static final的,对于注解来说没什么意义,而我们定义接口的方法就相当于注解的属性,也就对应了前面说的为什么注解只有成员属性,其实它就是接口的方法,这就是为什么成员属性会有括号,不同于接口我们可以在注解的括号中给成员变量赋值。
注解属性的类型:
- 基本数据类型;
- String;
- 枚举类型;
- 注解类型;
- Class类型;
- 以上类型的一维数组类型;
注解属性的赋值:
- 如果注解有多个属性,可以在注解的括号中使用逗号隔开,分别给对应的属性赋值;
- value 是定义注解时默认的属性,使用注解时括号中可以不写 value,直接写它对应的值即可;
- 属性可以 根据关键字 default 增加默认值,使用注解时,如果使用该属性的默认值,该属性可以不写;

@Inherited @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD,ElementType.PARAMETER,ElementType.TYPE}) public @interface NotNull { String value(); String name() default "Jack"; }

// value可以不写,直接写value对应的值 @NotNull("test") public class App { // name有默认的值Jack,所以也可以不写 @NotNull(value = "test") public static void main(@NotNull(value = "test",name = "lucy") String[] args) { new PlayGame(); } }
获取注解属性
前面我们讲了注解及其属性如何定义和使用,现在我们开始讲一下注解属性的提取,这才是使用注解的关键,获取属性的值才是使用注解的目的。
如何获取注解的属性呢?一般是使用反射,有三个基本的方法:
/**是否存在对应的注解(Annotation)对象*/ public boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) { return GenericDeclaration.super.isAnnotationPresent(annotationClass); } /**获取注解(Annotation)对象*/ public <A extends Annotation> A getAnnotation(Class<A> annotationClass) { Objects.requireNonNull(annotationClass); return (A) annotationData().annotations.get(annotationClass); } /**获取所有 注解(Annotation)对象数组*/ public Annotation[] getAnnotations() { return AnnotationParser.toArray(annotationData().annotations); }
下面结合前面的例子,我们来获取一下注解属性,在获取之前我们自定义的注解必须使用元注解@Retention(RetentionPolicy.RUNTIME)。

@Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface TestAnnotation { String name() default "Jack"; int age() default 25; }
注解 TestAnnotation,用于注解 Father 类。

@Target(ElementType.FIELD) @Retention(RetentionPolicy.RUNTIME) public @interface Age { int value() default 22; }
注解 Age,用于注解 Father 类的 age 属性。

@TestAnnotation(name = "小明",age = 30) public class Father { @Age(value = 35) private int age; }
Father 类,分别使用注解 TestAnnotation 和 Age 作用于类和属性。

/** * 游戏注解,游戏属性代表游戏名称 */ @Repeatable(People.class) @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface Game { String value() default "王者"; }
注解 Game,属性代表游戏名称,使用了元注解 @Repeatable(People.class) 代表可以同时作用于 PlayGame 类多次。

/** * 一个人喜欢玩游戏,他喜欢玩英雄联盟,绝地求生,极品飞车,尘埃4等 * 则我们需要定义一个People的注解,他属性代表喜欢玩游戏集合 */ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE,ElementType.METHOD}) public @interface People { Game[] value(); }
注解 People,属性代表喜欢玩游戏的数组。属性的类型为上面定义的注解 Game。

/** * 玩游戏类 */ public class PlayGame { @Game(value = "LOL") @Game(value = "PUBG") @Game(value = "NFS") @Game(value = "Dirt4") private void play() { } }
PlayGame 类,多次使用 @Game 注解作用于 play() 方法。
public class App { public static void main(String[] args) { /** * 获取类注解属性 */ // Class<?> fatherClass = Class.forName("code.annotation.TestAnnotation.Father"); Class<Father> fatherClass = Father.class; boolean annotationPresent = fatherClass.isAnnotationPresent(TestAnnotation.class); if(annotationPresent){ TestAnnotation annotation = fatherClass.getAnnotation(TestAnnotation.class); System.out.println("**********类注解**********"); System.out.println(annotation.name()); System.out.println(annotation.age()); } /** * 获取属性注解属性 */ try { Field age = fatherClass.getDeclaredField("age"); if(age.isAnnotationPresent(Age.class)){ Age annotation = age.getAnnotation(Age.class); System.out.println("**********属性注解**********"); System.out.println(annotation.value()); } } catch (NoSuchFieldException e) { e.printStackTrace(); } /** * 获取方法注解属性 */ try{ Class<PlayGame> playGameClass = PlayGame.class; Method playMethod = playGameClass.getDeclaredMethod("play"); if (playMethod.isAnnotationPresent(People.class)){ People annotation = playMethod.getAnnotation(People.class); Game[] values = annotation.value(); System.out.println("**********方法注解**********"); for (Game game : values) { System.out.println(game.value()); } } }catch (NoSuchMethodException e) { e.printStackTrace(); } } }
运行结果:
三、常用注解
1、@Override
- 它是用来描述当前方法是一个重写的方法,在编译阶段对方法进行检查;
- jdk1.5中它只能描述继承中的重写,jdk1.6中它可以描述接口实现的重写,也能描述类的继承的重写;
2、@Deprecated
- 它是用于描述当前方法是一个过时的方法;
3、@Test
- 一般用于描述方法,表示该方法可以不用 main 方法调用就可以测试出运行结果;
四、注解的应用和作用
1、使用注解进行参数配置
下面我们来看一个银行转账的例子,假设银行有个转账业务,转账的限额可能会根据汇率的变化而变化,我们可以利用注解灵活配置转账的限额,而不用每次都去修改我们的业务代码。

@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface BankTransferMoney { double maxMoney() default 10000; }

public class BankService { /** * @param money 转账金额 */ @BankTransferMoney(maxMoney = 15000) public static void TransferMoney(double money){ System.out.println(processAnnotationMoney(money)); } private static String processAnnotationMoney(double money) { try { Method transferMoney = BankService.class.getDeclaredMethod("TransferMoney",double.class); // 判断BankTransferMoney类型的注释是否作用于TransferMoney方法 if(transferMoney.isAnnotationPresent(BankTransferMoney.class)){ // 返回BankTransferMoney类型的注释 BankTransferMoney annotation = transferMoney.getAnnotation(BankTransferMoney.class); // 获取该注释的属性赋值 double maxMoney = annotation.maxMoney(); if(money > maxMoney){ return "转账金额大于限额,转账失败"; }else { return"转账金额为:"+money+",转账成功"; } } } catch (NoSuchMethodException e) { e.printStackTrace(); } return "转账处理失败"; } }
public class App { public static void main(String[] args) { BankService.TransferMoney(10000); } }
运行结果为:
通过上面的例子,只要汇率变化,我们就改变注解的配置值就可以直接改变当前最大限额。
2、第三方框架的使用
现在非常流行的 SpringMVC、SpringBoot 等框架里面都在广泛使用注解,如果我们要了解这些框架的原理,则注解的基础知识则是必不可少的。
3、注解的作用
- 提供信息给编译器: 编译器可以利用注解来检测出错误或者警告信息,打印出日志;
- 编译阶段时的处理: 软件工具可以用来利用注解信息来自动生成代码、文档或者做其它相应的自动处理;
- 运行时处理: 某些注解可以在程序运行的时候接受代码的提取,自动做相应的操作;
- 如官方文档的那句话所说,注解能够提供元数据,转账例子中处理获取注解值的过程是我们开发者直接写的注解提取逻辑,处理提取和处理 Annotation 的代码统称为 APT(Annotation Processing Tool)。上面转账例子中的processAnnotationMoney方法就可以理解为APT工具类;