Skip to content

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      // true

3. 反射 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(基线)
反射 invoke3-50x 慢
反射 + setAccessible(true)3-10x 慢

💡 JIT 优化会把热点反射调用 inlining 到接近直接调用的速度,但首次调用慢得多。

经验

  • 反射用于 框架启动期 一次性扫描(OK)
  • 不要在 业务热点路径 频繁反射(慢)
  • 频繁调用可以缓存 Method 对象

7. 实战练习

文件内容
Person.java反射目标类
ReflectionBasics.javaClass 三种获取方式
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 对象有几种获取方式?区别?

  1. Person.class - 编译期就知道
  2. obj.getClass() - 运行时拿对象的类
  3. Class.forName("...") - 运行时按名字加载(最动态)

三种拿到的是同一个对象。

Q3: 反射有哪些性能问题?

  1. 找方法/字段慢
  2. invoke 比直接调用慢(虚拟分派)
  3. 安全检查(每次 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 全靠反射 + 注解
✅ 反射性能差,框架启动期用,业务热点慎用

🔗 导航