Skip to content

反射与注解

1. 这是什么

反射允许程序在运行时获取类、方法、字段、构造器等元信息,并动态操作对象。
注解则为程序提供结构化元数据,常常配合反射参与框架运行。

一句话理解:

  • 反射解决的是“运行时怎么知道对象长什么样、能做什么”
  • 注解解决的是“怎么把额外规则、标记、配置挂到代码上”

2. 为什么重要

Spring、MyBatis、JUnit、Jackson 等大量框架能力,背后都依赖反射和注解。
理解它们,是进入 Java 框架世界的重要前提。

很多“看上去像魔法”的能力,本质上都是:

  • 先通过反射拿到类和方法信息
  • 再读取注解上的配置
  • 最后按规则执行增强逻辑

3. 先建立直觉:反射和注解分别负责什么

可以把它们理解成两个角色:

  • 反射:负责“看”和“调”
  • 注解:负责“标”

举个框架感知很强的抽象过程:

  1. 某个类上的方法标了 @GetMapping("/users")
  2. 框架启动时扫描这个类
  3. 通过反射找到这个方法
  4. 通过反射读取注解中的 "/users"
  5. 把 URL 和方法建立映射关系

所以:

  • 注解本身不会执行逻辑
  • 真正执行逻辑的是“注解解析器”
  • 反射通常是解析器的基础能力

4. 核心内容

4.1 Class 对象是什么

在 Java 里,每个类在运行时都有一个对应的 Class 对象,里面保存了类的元信息,例如:

  • 类名
  • 包名
  • 父类
  • 接口
  • 字段
  • 方法
  • 构造器
  • 注解

你可以把 Class 理解成“运行时的类说明书”。

4.2 获取 Class 对象的常见方式

最常见有三种:

java
UserService.class
obj.getClass()
Class.forName("com.example.UserService")

适用场景:

  • 类名.class:编译期已知类型,最直接
  • obj.getClass():已经有对象实例
  • Class.forName(...):只知道类的全限定名,常见于框架、插件、配置化加载

4.3 反射能做哪些事

常见能力包括:

  • 读取字段信息
  • 调用方法
  • 调用构造器创建对象
  • 判断类型关系
  • 读取注解

示意代码:

java
Class<?> clazz = UserService.class;
Field field = clazz.getDeclaredField("name");
Method method = clazz.getDeclaredMethod("hello");
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);

4.4 getFieldgetDeclaredField 的区别

这是非常常见的细节:

  • getField:只能拿到 public 字段,且会查父类
  • getDeclaredField:拿当前类自己声明的字段,不限访问修饰符

方法和构造器也有类似区别:

  • getMethod / getDeclaredMethod
  • getConstructor / getDeclaredConstructor

4.5 为什么有时要 setAccessible(true)

如果你想访问:

  • private 字段
  • private 方法
  • private 构造器

通常需要:

java
field.setAccessible(true);
method.setAccessible(true);

这意味着:

  • 你在绕过正常封装边界
  • 它很强大,但也要谨慎使用

在较新的 Java 环境和模块系统下,反射访问还可能受到额外限制。

4.6 注解怎么定义

定义注解本质上是定义一个特殊接口:

java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Route {
    String path();
}

关键点有两个:

  • @Target:这个注解能标在哪
  • @Retention:这个注解能保留到什么时候

4.7 注解保留策略为什么重要

常见保留策略有三种:

  • SOURCE:只在源码中存在,编译后丢弃
  • CLASS:保留到字节码,但运行时默认反射读不到
  • RUNTIME:运行时还能通过反射读取

如果你的目标是:

  • 启动时扫描注解
  • 运行时通过反射解析注解

那必须使用:

java
@Retention(RetentionPolicy.RUNTIME)

4.8 注解本身不执行逻辑

这一点特别容易误解。
注解只是元数据,它不会自己“生效”。

例如你写了:

java
@Audit
public void save() {}

如果没有解析器去扫描 @Audit 并执行增强逻辑,那么这行注解本身什么都不会做。

4.9 反射的价值和代价

反射的价值:

  • 灵活
  • 动态
  • 适合框架扩展、插件机制、通用工具

反射的代价:

  • 代码可读性变差
  • 类型检查从编译期后移到运行期
  • 性能通常不如直接调用
  • 容易破坏封装

工程上的建议是:

  • 框架层、基础设施层适度使用
  • 业务代码里不要为“炫技”而过度依赖反射

5. 学习重点

这一章最重要的不是记 API,而是理解:

  • Class 是运行时元信息入口
  • 反射是“运行时动态访问”的工具
  • 注解是元数据,不是执行器
  • 要让注解在运行时可读,必须用 RetentionPolicy.RUNTIME
  • 反射和注解经常一起出现,但职责不同

6. 常见问题

6.1 把注解当成“自动生效”的魔法

很多初学者以为只要写了注解就会生效。
其实真正执行的是:

  • 框架
  • 注解处理器
  • 反射扫描逻辑

6.2 不理解保留策略导致读取不到

如果注解不是 RUNTIME,你在运行时通过反射大概率读不到。

6.3 过度依赖反射而忽视可维护性

反射很强,但过量使用会让代码:

  • 跳转困难
  • 调试困难
  • 错误延迟到运行时

6.4 混淆 getMethodgetDeclaredMethod

这会导致你:

  • 明明方法存在,却拿不到
  • 或者只能拿到 public 方法,读不到私有方法

7. 动手验证

这一节可以直接复制运行,边看边验证。

7.1 准备一个可运行示例

新建文件 ReflectionAnnotationDemo.java,内容如下:

java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class ReflectionAnnotationDemo {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    @interface Route {
        String path();
    }

    @Retention(RetentionPolicy.CLASS)
    @Target(ElementType.TYPE)
    @interface ClassOnlyMarker {
    }

    @ClassOnlyMarker
    static class UserService {
        private String name;

        public UserService(String name) {
            this.name = name;
        }

        @Route(path = "/users")
        private String hello(String prefix) {
            return prefix + ", " + name;
        }
    }

    public static void main(String[] args) throws Exception {
        Class<UserService> classByLiteral = UserService.class;
        UserService service = new UserService("Codex");
        Class<?> classByInstance = service.getClass();
        Class<?> classByName = Class.forName("ReflectionAnnotationDemo$UserService");
        System.out.println("sameClassObject="
                + (classByLiteral == classByInstance && classByInstance == classByName));

        Constructor<UserService> constructor = classByLiteral.getConstructor(String.class);
        UserService newService = constructor.newInstance("Reflective");
        System.out.println("constructedClass=" + newService.getClass().getSimpleName());

        Field field = classByLiteral.getDeclaredField("name");
        field.setAccessible(true);
        field.set(newService, "ChangedByReflection");
        System.out.println("fieldValue=" + field.get(newService));

        Method method = classByLiteral.getDeclaredMethod("hello", String.class);
        method.setAccessible(true);
        Object result = method.invoke(newService, "hi");
        System.out.println("privateMethodResult=" + result);

        Route route = method.getAnnotation(Route.class);
        System.out.println("routePath=" + route.path());
        System.out.println("routeAnnotationPresent=" + method.isAnnotationPresent(Route.class));
        System.out.println("classOnlyMarkerPresent="
                + classByLiteral.isAnnotationPresent(ClassOnlyMarker.class));
    }
}

7.2 编译并运行

bash
javac ReflectionAnnotationDemo.java
java ReflectionAnnotationDemo

如果你在 PowerShell 中操作,也直接执行同样两条命令即可。

7.3 你应该观察到什么

输出不一定逐字完全一致,但应包含这些关键信息:

text
sameClassObject=true
constructedClass=UserService
fieldValue=ChangedByReflection
privateMethodResult=hi, ChangedByReflection
routePath=/users
routeAnnotationPresent=true
classOnlyMarkerPresent=false

7.4 每一行在验证什么

  • sameClassObject=true:说明三种获取 Class 的方式最终拿到的是同一个运行时类对象
  • constructedClass=UserService:说明可以通过反射调用构造器创建对象
  • fieldValue=ChangedByReflection:说明可以通过反射访问并修改私有字段
  • privateMethodResult=hi, ChangedByReflection:说明可以通过反射调用私有方法
  • routePath=/users:说明运行时注解可以被反射读取
  • routeAnnotationPresent=true:说明 RetentionPolicy.RUNTIME 的注解在运行时确实可见
  • classOnlyMarkerPresent=false:说明 RetentionPolicy.CLASS 的注解默认不能通过运行时反射读取

7.5 再做两个延伸验证

你可以继续做下面两个实验:

  1. 去掉 field.setAccessible(true)method.setAccessible(true)
  2. @Retention(RetentionPolicy.RUNTIME) 改成 CLASS

你可以观察:

  • 不开放访问时,私有成员无法直接反射访问
  • 注解不是 RUNTIME 时,运行时反射就读不到它

8. 练习建议

下面这些练习做完,这一章会更扎实:

  • 自定义一个注解,并在运行时扫描类上的所有标注方法
  • 用反射调用对象方法并设置字段值
  • 对比 getMethodgetDeclaredMethod 的行为差异
  • 写一个简单的“注解驱动路由注册” demo
  • 总结反射和注解在常见框架中的应用场景

9. 自测问题

  • Class 对象能提供哪些运行时信息?
  • getMethodgetDeclaredMethod 有什么区别?
  • 注解为什么通常要配合反射使用?
  • 注解保留策略对运行时行为有什么影响?
  • 为什么说注解本身不执行逻辑?

10. 自测核对要点

如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:

  • Class 是反射入口,提供类的元信息
  • 反射可以读取字段、调用方法、调用构造器、读取注解
  • 注解是元数据,不是逻辑执行器
  • 运行时读取注解通常要求 RetentionPolicy.RUNTIME
  • 反射很强大,但会带来性能、封装和可维护性代价