注解、自定义注解与元注解 | JavaSE
注解、自定义注解与元注解
一、学习目标
学完本章,你应该能够:
- 准确区分注释(Comment)与注解(Annotation)。
- 解释注解为什么是一种程序元数据。
- 使用
@interface自定义注解。 - 定义注解元素,并使用
default设置默认值。 - 理解
value元素的特殊简写规则。 - 掌握
@Target与@Retention两个核心元注解。 - 理解
SOURCE、CLASS、RUNTIME三种保留策略。 - 了解
@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 知识问答
- 什么是注解?
- 注解和注释有什么根本区别?
- 什么叫元数据?
- 注解自己会不会自动执行程序?
- 如何使用
@interface定义注解? default有什么作用?value为什么具有特殊简写规则?- 注解元素允许哪些主要类型?
- 什么是元注解?
@Target与@Retention分别解决什么问题?SOURCE / CLASS / RUNTIME有什么区别?- 为什么运行时反射解析通常需要
RUNTIME? @Documented有什么作用?@Inherited的继承边界是什么?
7.2 代码阅读
阅读:
@Target({
ElementType.TYPE,
ElementType.METHOD
})
@Retention(
RetentionPolicy.RUNTIME
)
public @interface MyConfig {
String value();
int priority() default 1;
}
回答:
MyConfig可以标记在哪里?- 能否标记字段?
- 注解会保留到什么时候?
value是否必须提供?priority是否必须提供?@MyConfig("user")是否可以?- 为什么以后可以通过反射读取它?
7.3 手写代码
从零定义:
@MyBook
要求包含:
name : String
age : int,默认 18
address : String[]
并标记一个 Book 类。
7.4 Debug
下面注解:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface MyTest {
}
需求却是:
程序运行以后使用反射扫描所有带
@MyTest的方法。
回答:
- 当前设计能否满足需求?
- 问题出在
@Target还是@Retention? - 为什么?
- 应该改成什么策略?
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 注解的核心知识模型。