注解、自定义注解与元注解 | JavaSE

注解、自定义注解与元注解

一、学习目标

学完本章,你应该能够:

  • 准确区分注释(Comment)与注解(Annotation)。
  • 解释注解为什么是一种程序元数据。
  • 使用 @interface 自定义注解。
  • 定义注解元素,并使用 default 设置默认值。
  • 理解 value 元素的特殊简写规则。
  • 掌握 @Target@Retention 两个核心元注解。
  • 理解 SOURCECLASSRUNTIME 三种保留策略。
  • 了解 @Documented@Inherited 等常见元注解的作用。
  • 为下一章“注解解析 + 反射”建立完整概念基础。

二、核心知识

2.1 什么是注解

我们已经见过很多:

@Override

@Test

@Deprecated

@FunctionalInterface

这种以:

@

开头的特殊标记,就是:

注解(Annotation)

可以先建立一个非常重要的理解:

注解是附加在 Java 程序元素上的结构化元数据。

所谓:

元数据(Metadata)

可以理解成:

描述数据的数据。

例如:

@Test
void calculatePrice() {
}

真正的业务代码是:

void calculatePrice() {
}

而:

@Test

是在描述这个方法:

“这是一个测试方法。”


2.2 注解自己会执行代码吗

不会因为写了:

@Test

这个方法就神奇地自己运行。

真正的过程应该理解为:

方法
+
@Test 元数据
     ↓
JUnit 读取这些信息
     ↓
判断这是测试方法
     ↓
调用这个方法

所以:

注解本身主要负责提供信息,真正决定如何处理这些信息的是编译器、工具、框架或我们的程序。

这句话非常重要。


2.3 注解和注释有什么区别

Java 注释:

// 这是测试方法

主要给:

程序员阅读

一般不会作为普通程序运行逻辑的一部分。

而注解:

@Test

是具有:

明确类型
明确语法
明确结构

的程序元数据。

编译器、工具或运行时程序能够按照规则处理。

因此:

注释 Comment
→ 主要解释代码给人看

注解 Annotation
→ 为程序元素附加结构化元数据

不要因为中文名字相近,把:

注释
注解

混为一谈。


2.4 注解可以标记什么

注解可以根据自身定义作用在不同位置,例如:

类
接口
方法
构造器
字段
参数
局部变量
类型使用位置
……

但一个具体注解究竟能写在哪里:

由这个注解自己的定义决定。

例如:

@Override

不能随便标在成员变量上。

原因不是:

“程序员约定不能”

而是:

该注解的适用位置受到其元数据定义约束。

后面会学习:

@Target

2.5 什么是自定义注解

Java 不只允许使用:

@Override
@Test
@Deprecated

这样的现成注解。

我们也可以定义自己的注解。

语法:

public @interface MyAnnotation {

}

注意:

@interface

是定义:

注解接口(Annotation Interface)

的专用语法。

例如:

public @interface MyTest {

}

现在就定义了一个:

@MyTest

注解。


2.6 最简单的标记注解

public @interface MyTest {

}

没有任何元素。

使用:

@MyTest
public void testLogin() {

}

它只是表达:

这个方法
拥有 @MyTest 标记

这样的注解称为:

标记注解(Marker Annotation)

它不需要额外携带数据。


2.7 注解可以携带数据

原课程中的 MyBook

public @interface MyBook {

    String name();

    int age() default 18;

    String[] address();
}

这里定义了三个注解元素:

name
age
address

使用:

@MyBook(
        name = "Java高级技术",
        age = 21,
        address = {
                "北京",
                "上海"
        }
)
public class BookDemo {

}

于是注解不只是:

有 / 没有

还携带了结构化配置:

name = Java高级技术
age = 21
address = 北京、上海

2.8 为什么它看起来像方法

定义:

String name();

int age();

String[] address();

看起来非常像接口中的抽象方法。

在 Java 语言层面,它们定义的是:

注解接口元素(Annotation Interface Element)

使用注解时:

@MyBook(
        name = "Java",
        age = 21,
        address = {"山西"}
)

就是在为这些元素提供值。

以后解析注解对象时,还会看到类似:

annotation.name();

annotation.age();

annotation.address();

这里先理解定义即可。


2.9 default 默认值

如果定义:

int age() default 18;

意味着使用:

@MyBook(
        name = "Java",
        address = {"山西"}
)

没有显式提供 age 时:

age = 18

如果没有:

default

那么使用注解时通常必须提供这个元素的值。

例如:

String name();

没有默认值。

因此:

@MyBook(
        address = {"山西"}
)

缺少:

name

会产生编译问题。


2.10 value 的特殊规则

注解里有一个约定非常常见:

public @interface Action {

    String value();
}

正常可以:

@Action(value = "delete")

如果使用的是单元素注解形式,并且元素名称就是:

value

可以简写:

@Action("delete")

所以:

@Action("delete")

其实就是:

@Action(value = "delete")

这就是为什么很多 Java 框架注解可以写得很短。

例如以后可能看到类似:

@GetMapping("/users")

而不是:

@GetMapping(value = "/users")

2.11 注解元素允许哪些类型

Java 对注解元素类型有明确限制。

主要允许:

基本数据类型
String
Class
枚举类型
注解类型
以上类型的一维数组

例如:

public @interface Config {

    int count();

    String name();

    Class<?> type();

    Level level();

    String[] tags();
}

其中:

enum Level {
    LOW,
    HIGH
}

这些都是合法方向。

不能随意写:

List<String> names();

也不能把任意普通 Java 对象类型作为注解元素类型。

这是因为:

注解不是一个随便存放任意运行时对象的容器,而是受 Java 语言规范严格约束的元数据结构。


2.12 注解定义和注解使用要区分

定义:

public @interface MyBook {

    String name();

    int age() default 18;
}

这是:

定义一种注解接口

使用:

@MyBook(
        name = "Java",
        age = 21
)
public class Demo {

}

这是:

创建并应用一个该类型的注解

要区分:

@interface
→ 定义

@MyBook(...)
→ 使用

三、使用方法

3.1 定义 MyBook

按照原课程案例:

public @interface MyBook {

    String name();

    int age() default 18;

    String[] address();
}

使用:

@MyBook(
        name = "Java高级技术",
        age = 21,
        address = {
                "太原",
                "北京"
        }
)
public class AnnotationDemo {

}

如果不设置 age

@MyBook(
        name = "Java高级技术",
        address = {"太原"}
)
public class AnnotationDemo {

}

则使用:

18

这个默认值。


3.2 定义只有 value 的注解

public @interface Action {

    String value();
}

使用:

@Action("save")
public void save() {

}

等价于:

@Action(value = "save")
public void save() {

}

这是非常常见的 Java 注解设计。


3.3 什么是元注解

现在出现一个新问题。

我们定义:

public @interface MyTest {

}

但是:

MyTest 能放在哪里?
MyTest 能活多久?
MyTest 会不会进入 Javadoc?
MyTest 是否能被子类继承?

这些信息也是:

描述注解的信息。

因此 Java 提供:

元注解(Meta-Annotation)

可以理解为:

用来修饰注解接口声明的注解。

例如:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

}

这里:

@Target
@Retention

就是元注解。


3.4 @Target:限制注解使用位置

例如:

@Target(ElementType.METHOD)
public @interface MyTest {

}

意味着:

@MyTest
只能用于方法

合法:

@MyTest
public void testLogin() {

}

如果拿去标字段:

@MyTest
private int age;

编译器会拒绝。

因此:

@Target 决定“这个注解可以写在哪里”。


3.5 同时允许多个位置

原课程:

@Target({
        ElementType.METHOD,
        ElementType.FIELD
})
public @interface MyTest1 {

}

表示:

方法
+
字段

都允许使用。

例如:

@MyTest1
private int age;

@MyTest1
public void testAge() {

}

3.6 常见 ElementType

初学阶段最常见:

| ElementType | 典型作用位置 | | ------------------ | ------------------ | | TYPE | 类、接口等类型声明 | | FIELD | 字段 | | METHOD | 方法 | | PARAMETER | 参数 | | CONSTRUCTOR | 构造器 | | LOCAL_VARIABLE | 局部变量 | | ANNOTATION_TYPE | 注解接口声明 | | TYPE_PARAMETER | 类型参数 | | TYPE_USE | 类型使用位置 | | PACKAGE | 包 | | MODULE | 模块 | | RECORD_COMPONENT | record 组件 |

例如:

@Target({
        ElementType.TYPE,
        ElementType.METHOD
})
public @interface MyConfig {

}

允许:

@MyConfig
public class UserService {

    @MyConfig
    public void save() {

    }
}

3.7 如果不写 @Target 呢

如果自定义注解没有声明:

@Target(...)

则它并不是:

哪里都不能用

相反,它可作为修饰符用于各种声明位置。

实际工程中,如果用途明确:

建议使用 @Target 清晰约束其合法使用场景。

这样可以让编译器替我们防止误用。


3.8 @Retention:控制保留周期

第二个最核心的元注解:

@Retention(...)

决定:

注解保留到程序生命周期的哪个阶段。

有三种策略:

SOURCE
CLASS
RUNTIME

3.9 RetentionPolicy.SOURCE

@Retention(
        RetentionPolicy.SOURCE
)

表示:

.java 源代码阶段存在
        ↓
编译时可以使用
        ↓
不会保留到 class 文件供运行时读取

可以理解:

源码级注解

它常适合:

编译器检查
源码处理

3.10 RetentionPolicy.CLASS

@Retention(
        RetentionPolicy.CLASS
)

表示:

.java
 ↓
.class 中仍然记录
 ↓
运行时 JVM 不保证作为运行时注解提供给反射读取

而且:

CLASS 是未显式声明 @Retention 时的默认保留策略。


3.11 RetentionPolicy.RUNTIME

@Retention(
        RetentionPolicy.RUNTIME
)

表示:

源码
 ↓
class 文件
 ↓
JVM 运行期间
 ↓
仍可以通过反射读取

这是以后:

反射 + 注解

最重要的一种。

例如:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

}

如果以后希望程序运行时检查:

某个 Method 上有没有 @MyTest

就需要:

RUNTIME

3.12 三种 Retention 的完整模型

SOURCE

.java
 ✓
.class
 ✗
运行时
 ✗
CLASS

.java
 ✓
.class
 ✓
运行时反射
 ✗
RUNTIME

.java
 ✓
.class
 ✓
运行时反射
 ✓

如果将来要:

通过反射解析注解

通常最关键的是:

@Retention(
        RetentionPolicy.RUNTIME
)

3.13 定义完整 MyTest

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

    int count() default 1;
}

这里表达:

@Target(METHOD)
→ 只能标方法

@Retention(RUNTIME)
→ 运行期间仍然存在

count default 1
→ 可以附带执行次数信息

使用:

@MyTest
public void testLogin() {

}

等价于:

@MyTest(count = 1)
public void testLogin() {

}

还可以:

@MyTest(count = 3)
public void testRegister() {

}

至于以后怎么读取:

count = 3

并据此执行三次,

留到下一章:

10-09 注解解析与反射协作。


3.14 @Documented

还有一个常见元注解:

@Documented

它表示:

被该注解标记的信息应该被视为被标记程序元素公共文档的一部分。

例如:

@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Api {

}

配合 Javadoc 等工具时,该注解信息可以进入生成的 API 文档。

它主要影响:

文档呈现

而不是程序业务行为。


3.15 @Inherited

例如:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {

}

父类:

@Role
public class Person {

}

子类:

public class Student
        extends Person {

}

@Inherited 能影响:

对类层级进行注解查询时的继承行为。

需要特别注意:

它主要针对类的继承链

不是:

所有方法、字段、接口上的注解
都会自动继承。

尤其:

实现接口并不会因为接口上的 @Inherited 注解而自动获得相同的继承查询效果。


四、原理与进阶

4.1 注解从语言层面到底是什么

使用:

public @interface MyTest {
}

定义的是:

注解接口(Annotation Interface)

所有注解接口都与:

java.lang.annotation.Annotation

体系关联。

因此:

注解不是注释字符串

它拥有明确的:

类型
元素定义
取值规则
适用位置
生命周期

4.2 @MyTest 本身和 @MyTest 的使用不是一个东西

定义:

public @interface MyTest {
}

表示:

一种注解类型

使用:

@MyTest
public void test() {
}

表示:

在 test 方法上存在一个 MyTest 注解实例

以后通过反射获得:

MyTest annotation = ...

时,获得的是该注解类型在目标元素上的运行时表示。


4.3 注解不负责业务逻辑

这一点必须再次强调。

例如:

@Transactional
public void transfer() {

}

真正让事务发生的不是:

@Transactional 这几个字符

而是:

Spring 等框架
读取这个注解
    ↓
识别其语义
    ↓
执行事务增强逻辑

所以:

注解
→ 描述“应该怎样处理”

框架
→ 真正负责“怎样处理”

下一章学习:

反射读取注解

以后,这个机制就会彻底打通。


4.4 注解让配置靠近代码

没有注解时,可能写:

testMethods=testLogin,testRegister

有注解:

@MyTest
public void testLogin() {
}

元数据直接跟:

被描述的程序元素

放在一起。

这样通常:

可读性更强
不容易和代码脱节
框架扫描更方便

这也是现代 Java 框架大量采用注解的重要原因之一。


五、实践应用

5.1 JUnit

@Test
void add() {

}

框架看到:

@Test

后决定:

这个方法属于测试方法。

5.2 Spring

以后会看到:

@Controller

@Service

@Repository

@Component

这些注解会给程序元素附加:

组件角色

等元数据信息。

框架读取后执行自己的管理逻辑。


5.3 Spring MVC

例如:

@GetMapping("/users")

它能够描述:

这个方法
对应某个 HTTP 请求映射

然后框架再根据这些元数据处理 Web 请求。


5.4 ORM 与对象映射

可能出现:

@Table("user")

@Column("user_name")

表示:

Java 类型 / 字段
和
数据库结构

之间的映射关系。

仍然是:

注解提供元数据
+
框架解析元数据
+
框架执行实际逻辑

六、常见问题

6.1 注解和注释是一个东西吗?

不是。

注释
→ 主要给程序员阅读

注解
→ 程序结构化元数据

6.2 写了注解以后功能就自动生效吗?

不一定。

必须存在:

编译器
工具
框架
注解处理器
运行时程序

去解释它。

否则一个自定义:

@MyTest

本身不会自动执行方法。


6.3 @interface 是 interface 前面加个 @ 吗?

语法外观看起来如此,但它有专门语义:

public @interface MyTest {
}

是在:

声明注解接口。

不是普通接口声明的简单装饰版本。


6.4 default 是必须的吗?

不是。

String name();

没有默认值:

使用注解时通常必须提供。

int age() default 18;

有默认值:

使用时可以省略。


6.5 value 为什么经常可以不写名字?

当使用符合单元素注解简写规则、元素名为:

value

时:

@Action("save")

可以代替:

@Action(value = "save")

6.6 @Target 和 @Retention 有什么区别?

一句话:

@Target
→ 管“用在哪里”

@Retention
→ 管“活多久”

这两个必须彻底区分。


6.7 运行时反射读取注解应该用哪个 Retention?

RetentionPolicy.RUNTIME

因为它会保留到 JVM 运行期间。


6.8 不写 @Retention 默认是什么?

默认:

RetentionPolicy.CLASS

不是:

RUNTIME

这是非常常见的易错点。


6.9 @Inherited 是不是所有注解都能继承?

不是。

而且它主要影响:

类声明上的注解
+
父类继承链查询

不能理解成:

方法
字段
接口
全部自动继承。

6.10 注解元素能不能写任意对象类型?

不能。

例如不能随意定义:

User value();

List<String> names();

注解元素类型由 Java 语言规范明确限制。


七、练习与验收

7.1 知识问答

  1. 什么是注解?
  2. 注解和注释有什么根本区别?
  3. 什么叫元数据?
  4. 注解自己会不会自动执行程序?
  5. 如何使用 @interface 定义注解?
  6. default 有什么作用?
  7. value 为什么具有特殊简写规则?
  8. 注解元素允许哪些主要类型?
  9. 什么是元注解?
  10. @Target@Retention 分别解决什么问题?
  11. SOURCE / CLASS / RUNTIME 有什么区别?
  12. 为什么运行时反射解析通常需要 RUNTIME
  13. @Documented 有什么作用?
  14. @Inherited 的继承边界是什么?

7.2 代码阅读

阅读:

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

    String value();

    int priority() default 1;
}

回答:

  1. MyConfig 可以标记在哪里?
  2. 能否标记字段?
  3. 注解会保留到什么时候?
  4. value 是否必须提供?
  5. priority 是否必须提供?
  6. @MyConfig("user") 是否可以?
  7. 为什么以后可以通过反射读取它?

7.3 手写代码

从零定义:

@MyBook

要求包含:

name    : String
age     : int,默认 18
address : String[]

并标记一个 Book 类。


7.4 Debug

下面注解:

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

需求却是:

程序运行以后使用反射扫描所有带 @MyTest 的方法。

回答:

  1. 当前设计能否满足需求?
  2. 问题出在 @Target 还是 @Retention
  3. 为什么?
  4. 应该改成什么策略?

7.5 综合训练

设计:

@Command

要求:

只能标记方法
运行期间可被反射读取

元素:
value    → 命令名称
count    → 执行次数,默认 1
enabled  → 是否启用,默认 true

然后使用:

@Command(
        value = "backup",
        count = 2
)
public void backup() {

}

本章只负责:

定义并正确使用注解

暂时不要编写反射解析器。

解析留到下一章。


7.6 本章验收

能够闭卷写出:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {

    int count() default 1;
}

并准确解释:

@interface
→ 定义注解接口

@Target
→ 可以写在哪里

@Retention
→ 可以保留多久

RUNTIME
→ 运行时可以通过反射读取

default
→ 默认元素值

value
→ 单元素注解的常用约定

最后能够口述:

程序元素
   +
Annotation 元数据
        ↓
框架 / 工具读取
        ↓
决定如何处理程序元素

就算真正建立了 Java 注解的核心知识模型。