主题
Chapter 19 · 反射与注解
反射让程序"看见自己",注解让代码"会说话" — Spring/JUnit/Mybatis 的灵魂。
🎯 本章目标
- 理解 反射(Reflection) 的本质
- 掌握
Class对象的 3 种获取方式 - 学会动态创建对象、调用方法、读写字段
- 理解 注解(Annotation) 的作用
- 写一个简单的 注解处理器(模仿 JUnit)
1. 什么是反射?
运行时检查和操作类的能力。
类比:你拿到一辆陌生汽车(对象),能凭"反射"知道:
- 这是什么牌子?(getClass)
- 有多少个轮子?(字段)
- 能做什么动作?(方法)
- 怎么发动?(调用方法)
代码层面:
java
Object obj = new Person();
// 不用反射:
Person p = (Person) obj;
p.sayHello();
// 用反射:运行时才知道是 Person,照样能调用方法
obj.getClass().getMethod("sayHello").invoke(obj);💡 反射 = 运行时拿到类的元数据 + 动态操作。
2. Class 对象:万物之源
每个类在 JVM 中都有一个唯一的 Class<?> 对象。
三种获取方式
java
// 1. 类字面量(最常用)
Class<Person> c1 = Person.class;
// 2. 对象的 getClass()
Person p = new Person();
Class<?> c2 = p.getClass();
// 3. Class.forName()(运行时动态)
Class<?> c3 = Class.forName("com.example.Person");💡 三种方式拿到的是 同一个对象!
java
c1 == c2 // true
c1 == c3 // true3. 反射 4 大常用操作
3.1 创建实例
java
Class<?> clazz = Class.forName("com.example.Person");
// 老 API(已不推荐)
Person p1 = (Person) clazz.newInstance(); // 调无参构造
// 新 API(推荐,JDK 9+)
Person p2 = (Person) clazz.getDeclaredConstructor().newInstance();
// 调有参构造
Person p3 = (Person) clazz.getDeclaredConstructor(String.class, int.class)
.newInstance("张三", 25);3.2 操作字段
java
Class<?> clazz = Person.class;
Person p = new Person("张三", 25);
// 公开字段
Field f = clazz.getField("name");
String name = (String) f.get(p);
f.set(p, "李四");
// 私有字段(要 setAccessible)
Field age = clazz.getDeclaredField("age");
age.setAccessible(true);
int a = (int) age.get(p);
age.set(p, 30);3.3 调用方法
java
Class<?> clazz = Person.class;
Person p = new Person();
// 公开方法
Method m = clazz.getMethod("sayHello");
m.invoke(p);
// 带参数
Method m2 = clazz.getMethod("greet", String.class);
m2.invoke(p, "张三");
// 私有方法
Method m3 = clazz.getDeclaredMethod("secret");
m3.setAccessible(true);
m3.invoke(p);3.4 看类的全部信息
java
Class<?> clazz = Person.class;
clazz.getName(); // 全限定名
clazz.getSimpleName(); // 短名
clazz.getSuperclass(); // 父类
clazz.getInterfaces(); // 实现的接口
clazz.getFields(); // 公开字段
clazz.getDeclaredFields(); // 所有字段(含 private)
clazz.getMethods(); // 公开方法(含继承)
clazz.getDeclaredMethods(); // 当前类的所有方法
clazz.getDeclaredConstructors();
clazz.getAnnotations(); // 注解4. 注解:给代码贴标签
4.1 已有注解
java
@Override // 重写父类方法
@Deprecated // 标记过时
@SuppressWarnings("unchecked") // 抑制警告
@FunctionalInterface // 函数式接口
@SafeVarargs // varargs 安全4.2 自定义注解
java
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 运行时保留
@Target(ElementType.METHOD) // 只能用在方法上
public @interface Test {
String description() default "";
int timeout() default 0;
}使用:
java
public class MyTest {
@Test(description = "测试加法")
public void testAdd() { ... }
@Test(description = "测试减法", timeout = 1000)
public void testSub() { ... }
}4.3 元注解(注解的注解)
| 元注解 | 作用 |
|---|---|
@Retention | 保留策略(SOURCE / CLASS / RUNTIME) |
@Target | 适用位置(METHOD / FIELD / TYPE / PARAMETER ...) |
@Documented | 出现在 javadoc |
@Inherited | 子类继承父类的注解 |
@Repeatable | 同一个注解可以重复使用 |
4.4 RetentionPolicy 三选一
| 策略 | 何时存在 | 用途 |
|---|---|---|
| SOURCE | 仅源码 | 编译后丢弃(如 @Override) |
| CLASS | 编译进 .class | 默认,但不进入 JVM |
| RUNTIME | 运行时还在 | 反射可读取(最常用) |
5. 实战:写一个迷你 JUnit
java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyTest { }
class TestRunner {
public static void run(Class<?> testClass) throws Exception {
Object instance = testClass.getDeclaredConstructor().newInstance();
for (Method m : testClass.getDeclaredMethods()) {
if (m.isAnnotationPresent(MyTest.class)) {
try {
m.invoke(instance);
System.out.println("✓ " + m.getName());
} catch (Exception e) {
System.out.println("✗ " + m.getName() + " : " + e.getCause().getMessage());
}
}
}
}
}
class MyTests {
@MyTest void test1() { System.out.println("跑 test1"); }
@MyTest void test2() { throw new RuntimeException("故意失败"); }
}
TestRunner.run(MyTests.class);输出:
跑 test1
✓ test1
✗ test2 : 故意失败6. 反射 vs 普通调用:性能
| 调用方式 | 相对性能 |
|---|---|
| 直接调用 | 1x(基线) |
| 反射 invoke | 3-50x 慢 |
| 反射 + setAccessible(true) | 3-10x 慢 |
💡 JIT 优化会把热点反射调用 inlining 到接近直接调用的速度,但首次调用慢得多。
经验:
- 反射用于 框架启动期 一次性扫描(OK)
- 不要在 业务热点路径 频繁反射(慢)
- 频繁调用可以缓存
Method对象
7. 实战练习
| 文件 | 内容 |
|---|---|
Person.java | 反射目标类 |
ReflectionBasics.java | Class 三种获取方式 |
ReflectionInvoke.java | 创建对象 / 调方法 / 读写字段 |
MyTest.java | 自定义注解 |
TestRunner.java | 模仿 JUnit 的执行器 |
MyTests.java | 测试用例 |
8. 浏览器演示
打开 demo.html:
- Class 三种获取方式
- 反射动态调用模拟
- 自定义注解执行流程
9. 面试可能会问什么?
Q1: 什么是反射?有什么用?
运行时检查和操作类的能力。 应用:
- Spring 依赖注入(IoC)
- ORM 框架(MyBatis、Hibernate)
- 单元测试(JUnit)
- JSON 序列化(Jackson、Gson)
- 动态代理
Q2: Class 对象有几种获取方式?区别?
Person.class- 编译期就知道obj.getClass()- 运行时拿对象的类Class.forName("...")- 运行时按名字加载(最动态)
三种拿到的是同一个对象。
Q3: 反射有哪些性能问题?
- 找方法/字段慢
- invoke 比直接调用慢(虚拟分派)
- 安全检查(每次 invoke 都要查权限)
优化:
- 缓存 Method/Field 对象
- 调
setAccessible(true)跳过安全检查 - 频繁场景考虑用 ASM / MethodHandle
Q4: 注解的 RetentionPolicy 三种含义?
- SOURCE:编译期丢弃(@Override)
- CLASS:进 .class,运行时丢弃(默认)
- RUNTIME:运行时也在,反射可读
Q5: @Target 有哪些常用值?
- TYPE:类、接口、注解
- METHOD:方法
- FIELD:字段
- PARAMETER:参数
- CONSTRUCTOR:构造器
- LOCAL_VARIABLE:局部变量
- ANNOTATION_TYPE:注解类型
Q6: 如何让一个注解可以多次使用?
JDK 8 加了 @Repeatable:
java
@Repeatable(Schedules.class)
public @interface Schedule { String day(); }
@interface Schedules { Schedule[] value(); }
@Schedule(day = "Mon")
@Schedule(day = "Tue")
public class Job { }🎁 本章小结
✅ 反射 = 运行时操作类(取信息、调方法、改字段)
✅ Class 对象三种获取:.class / getClass() / forName()
✅ 注解 = 元数据贴标签
✅ @Retention(RUNTIME) 才能反射读
✅ Spring/JUnit/MyBatis 全靠反射 + 注解
✅ 反射性能差,框架启动期用,业务热点慎用🔗 导航
- ⬅️ 上一章:Chapter 18 · Lambda 与 Stream
- ➡️ 下一章:Chapter 20 · JVM 内存模型与垃圾回收