注解

什么是注解?

一句话理解:注解就是给代码”贴标签”——告诉编译器或框架一些额外信息,但不影响代码本身的逻辑

比喻:代码是一件衣服,注解是衣服上的洗涤标签——衣服照样穿,但洗衣机(框架)会根据标签决定怎么处理

注解本身什么都不做,只提供元数据(metadata)

真正干活的是读取注解的代码(编译器或框架),它们根据注解信息决定行为

注解和注释的区别:

注释(//)是给人看的,编译后就没了

注解(@)是给程序看的,编译器和框架可以读取并处理

一、内置注解:Java自带的标签

@Override:我在重写父类方法

贴在方法上,告诉编译器”我是故意重写的,帮我检查一下有没有写错”

如果方法签名和父类不匹配,编译器直接报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testOverride() {
class Animal {
public void speak() {
System.out.println("...");
}
}
class Dog extends Animal {
@Override // ✅ 编译器会检查父类确实有speak()方法
public void speak() {
System.out.println("汪汪!");
}
// @Override // ❌ 如果写成speck(),编译器立刻报错
// public void speck() { }
}
new Dog().speak(); // 汪汪!
}

不加 @Override 也能重写,但强烈建议加——防止手滑写错方法名导致”重写变新方法”的bug

@Deprecated:我过时了,别用了

贴在类、方法或字段上,编译器会给调用者一个警告(划删除线)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void testDeprecated() {
class OldApi {
@Deprecated // 标记为过时
public void oldMethod() {
System.out.println("我是老方法,请用newMethod()代替");
}
public void newMethod() {
System.out.println("我是新方法");
}
}
OldApi api = new OldApi();
api.oldMethod(); // IDE会显示删除线⚠️
api.newMethod(); // ✅ 推荐使用
}

@SuppressWarnings:闭嘴,我知道我在干什么

告诉编译器”这个警告我知道了,别烦我”

常见参数:"unchecked"(泛型相关)、"deprecation"(使用过时API)、"all"(所有警告)

@SuppressWarnings("unchecked") 最常用

@FunctionalInterface:我是函数式接口

标记接口只能有一个抽象方法,否则编译报错

配合Lambda表达式使用

1
2
3
4
5
@FunctionalInterface
interface MyFunction {
int apply(int x);
// void another(); // ❌ 加第二个抽象方法会编译报错
}

内置注解速查表

注解 贴在哪 作用
@Override 方法 编译器检查是否正确重写
@Deprecated 类/方法/字段 标记过时,编译器给警告
@SuppressWarnings 类/方法/变量 抑制编译器警告
@FunctionalInterface 接口 确保只有一个抽象方法

二、元注解:给注解贴的注解

元注解是”注解的注解”——用来定义自定义注解的行为规则

@Target:这个注解能贴在哪?

能贴在哪
ElementType.TYPE 类、接口、枚举
ElementType.METHOD 方法
ElementType.FIELD 字段
ElementType.PARAMETER 方法参数
ElementType.CONSTRUCTOR 构造器
ElementType.LOCAL_VARIABLE 局部变量
ElementType.ANNOTATION_TYPE 注解(元注解自己就用这个)
ElementType.PACKAGE

@Retention:这个注解活到什么时候?(最重要的元注解)

生命周期 说明
RetentionPolicy.SOURCE 只在源码里 编译后就没了。如 @Override
RetentionPolicy.CLASS 存到class文件 但运行时读不到(默认值)
RetentionPolicy.RUNTIME 运行时还在 可以用 反射机制 读取。框架用的都是这个

口诀:想要运行时用反射读注解,必须设成 RUNTIME

@Documented

生成JavaDoc时把注解信息也包含进去

不常用,知道有这么个东西就行

@Inherited

父类上的这个注解会被子类继承

注意:只对类有效,接口和方法上的注解不会被继承

三、自定义注解

@interface 关键字定义,语法长得像接口但完全是两码事

基本语法

1
2
3
4
5
6
7
// 定义一个自定义注解
@Target(ElementType.METHOD) // 只能贴在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时可读取
@interface MyTest {
String value() default ""; // 注解的属性,带默认值
int priority() default 0; // 可以有多个属性
}

注解属性的规则

属性类型只能是:基本类型、String、Class、枚举、注解、以及它们的数组

属性用”方法”的语法声明,但实际上是属性

default 指定默认值,没有默认值的属性使用时必须填

如果只有一个属性且名叫 value,使用时可以省略 value=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@Test
public void testCustomAnnotation() throws Exception {
// 定义注解(实际项目中写在单独文件里,这里为了演示写在一起)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface ApiDoc {
String description();
String author() default "unknown";
int version() default 1;
}

// 使用注解
class UserApi {
@ApiDoc(description = "获取用户信息", author = "Lucy", version = 2)
public void getUser() {}

@ApiDoc(description = "删除用户") // author和version用默认值
public void deleteUser() {}
}

// 用反射读取注解(这就是框架干的事情)
for (Method method : UserApi.class.getDeclaredMethods()) {
ApiDoc doc = method.getAnnotation(ApiDoc.class);
if (doc != null) {
System.out.println("方法:" + method.getName());
System.out.println(" 描述:" + doc.description());
System.out.println(" 作者:" + doc.author());
System.out.println(" 版本:v" + doc.version());
}
}
}

四、注解处理:反射读取注解

注解本身不干活,真正的魔法在于读取注解并执行逻辑

这就需要用到 反射机制 中的 getAnnotation 系列方法

核心API

方法 说明
getAnnotation(注解类.class) 获取指定注解,没有返回null
getAnnotations() 获取所有注解(包括继承的)
getDeclaredAnnotations() 获取所有直接声明的注解
isAnnotationPresent(注解类.class) 判断是否有某个注解

实战:自己写一个简易测试框架

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Test
public void testMiniTestFramework() throws Exception {
// Step 1: 定义一个@MyTest注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@interface MyTest {
String name() default "";
}

// Step 2: 写测试类,用@MyTest标记测试方法
class CalculatorTest {
@MyTest(name = "加法测试")
public void testAdd() {
System.out.println(" 1 + 1 = " + (1 + 1) + " ✅");
}
@MyTest(name = "减法测试")
public void testSubtract() {
System.out.println(" 3 - 1 = " + (3 - 1) + " ✅");
}
public void helperMethod() {
// 没有@MyTest注解,不会被执行
System.out.println("我不是测试方法");
}
}

// Step 3: 框架代码——用反射找到所有@MyTest方法并执行
System.out.println("=== 开始执行测试 ===");
CalculatorTest testObj = new CalculatorTest();
int count = 0;
for (Method method : CalculatorTest.class.getDeclaredMethods()) {
if (method.isAnnotationPresent(MyTest.class)) {
MyTest annotation = method.getAnnotation(MyTest.class);
System.out.println("运行测试:" + annotation.name());
method.invoke(testObj);
count++;
}
}
System.out.println("=== 执行完毕,共 " + count + " 个测试 ===");
// 这就是JUnit的简化版原理!
}

注意:只有 @Retention(RetentionPolicy.RUNTIME) 的注解才能被反射读取

@OverrideSOURCE 级别的,编译后就消失了,反射读不到

框架用的注解(@Autowired@RequestMapping等)都是 RUNTIME 级别

五、实际框架中的注解原理(简述)

Spring的 @Autowired 怎么工作的?

  1. Spring启动时扫描所有类(通过 反射机制

  2. 找到所有带 @Component@Service@Controller 注解的类

  3. 通过反射创建这些类的对象(调用构造器)

  4. 扫描对象的所有字段,找到带 @Autowired

  5. 通过反射 field.setAccessible(true) + field.set() 注入依赖

所以 @Autowired 的private字段能被赋值,就是因为Spring用了暴力反射

Spring MVC的 @RequestMapping 怎么工作的?

  1. 扫描所有 @Controller

  2. 找到每个方法上的 @RequestMapping 注解

  3. 读取注解的value属性(URL路径)和method属性(GET/POST)

  4. 建立 URL → Method 的映射表

  5. 请求来了 → 查映射表 → 反射调用对应方法

MyBatis的 @Select 怎么工作的?

  1. 扫描Mapper接口的所有方法

  2. 读取 @Select("SQL语句") 注解

  3. 方法被调用时 → 执行注解里的SQL → 结果通过反射映射到返回类型

小结:框架 = 注解(标记) + 反射(执行)

注解负责”声明意图”(我想注入、我想处理这个URL、我想执行这个SQL)

反射负责”实现意图”(创建对象、调用方法、赋值字段)

六、常见坑

坑1:注解@Retention没设成RUNTIME

自定义注解默认是 CLASS 级别,运行时反射读不到

99%的情况你想用 RUNTIME,记得加 @Retention(RetentionPolicy.RUNTIME)

坑2:注解属性类型限制

不能用包装类型(Integer),只能用基本类型(int

不能用任意对象,只能用 String、Class、枚举、注解和它们的数组

原因:注解的值在编译时就确定了,必须是常量

坑3:@Inherited只对类有效

接口上的注解不会被实现类继承

方法上的注解不会被重写方法继承

只有类上的注解会被子类继承

坑4:注解属性是数组时的写法

只有一个元素可以省略花括号:@Target(ElementType.METHOD)

多个元素必须用花括号:@Target({ElementType.METHOD, ElementType.TYPE})

坑5:忘了注解和注释的区别

// 这是注释 → 给人看的

@Override → 给程序看的

面试时问到”注解是什么”,千万别说成注释

七、对比表格

四种内置注解对比

注解 级别 目的 不用会怎样
@Override SOURCE 编译检查重写 方法名写错不会报错
@Deprecated RUNTIME 标记过时API 调用者不知道该用新方法
@SuppressWarnings SOURCE 抑制警告 IDE一堆黄色警告看着烦
@FunctionalInterface RUNTIME 确保函数式接口 多写了抽象方法不会报错

@Retention三种级别对比

级别 存在时间 能用反射读吗 典型代表
SOURCE 编译时就没了 @Override
CLASS 存到class文件 默认值(很少用)
RUNTIME 运行时还在 @Autowired、@RequestMapping

八、练习题

练习1(基础):定义一个 @Author 注解,包含 name 和 date 两个属性,贴在类上,用反射读取并打印

练习2(进阶):模仿Spring的 @Autowired,写一个简单的自动注入:定义 @Inject 注解,写一个方法扫描对象的所有字段,如果有 @Inject 注解就自动创建并注入对象

练习3(综合):写一个 @Validate 注解(属性:min、max),贴在字段上,写一个校验方法检查对象的所有 @Validate 字段是否在范围内。这就是 Bean Validation(如 @Size@Min)的简化版原理


上一章 目录 下一章
反射机制 java基础 枚举类型