Skip to content

反射与注解 ​

Java 中级 · Java 进阶

反射(Reflection)让程序在运行时检查、调用类与对象;注解(Annotation)是贴在代码上的元数据,两者结合构成了 Spring、MyBatis 等几乎所有框架的底层基础。理解它俩,才算真正看懂框架是怎么「反着控制」你的代码的。

一、反射的本质:Class 对象 ​

JVM 加载一个类时,会在方法区创建一份类元信息,并在堆中生成一个 java.lang.Class 对象作为它的访问入口。反射的一切都从这个 Class 对象开始。

1.1 获取 Class 对象的三种方式 ​

java
// 1. 类字面量(不触发初始化,最安全、最快)
Class<?> c1 = User.class;

// 2. 对象.getClass()
User u = new User();
Class<?> c2 = u.getClass();

// 3. Class.forName()(会触发类初始化,即执行 static 块;是 JDBC 老式驱动加载方式)
Class<?> c3 = Class.forName("com.demo.User");

System.out.println(c1 == c2 && c2 == c3); // true —— 同一个类只有一份 Class 对象

三者区别

User.class 不会触发静态初始化;Class.forName("...") 会触发静态初始化(执行 static {} 块)。这也是为什么老代码用 Class.forName("com.mysql.jdbc.Driver") 能加载驱动——它顺带触发了驱动的 static 注册块。

1.2 反射操作成员 ​

java
Class<?> clazz = User.class;

// 构造对象(JDK 9+ 推荐用 getDeclaredConstructor)
User u = (User) clazz.getDeclaredConstructor().newInstance();

// 字段:私有字段要 setAccessible(true) 绕过访问检查
Field nameField = clazz.getDeclaredField("name");
nameField.setAccessible(true);
nameField.set(u, "Tom");
System.out.println(nameField.get(u)); // Tom

// 方法
Method setName = clazz.getDeclaredMethod("setName", String.class);
setName.invoke(u, "Jerry");

// 注解
if (clazz.isAnnotationPresent(Component.class)) {
    System.out.println(clazz.getAnnotation(Component.class).value());
}
API说明
getDeclaredXxx获取本类声明的成员(含私有),不含继承
getXxx获取public 成员,含继承
setAccessible(true)关闭访问检查(模块化后对 module 外可能有 InaccessibleObjectException)

二、注解:元数据与保留策略 ​

2.1 元注解 ​

元注解作用
@Retention保留到哪个阶段:SOURCE(编译丢弃)/CLASS(进 class 文件但不进内存)/RUNTIME(反射可读)
@Target能贴在哪些位置(类/方法/字段/参数…)
@Documented生成 javadoc 时包含
@Inherited子类可继承父类的注解
@Repeatable允许同一位置重复标注

2.2 自定义注解 ​

java
@Retention(RetentionPolicy.RUNTIME)   // 必须 RUNTIME,反射才能读
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface RateLimit {
    int qps() default 100;   // 注解属性,本质是方法
    String key() default "";
}

// 使用
@RateLimit(qps = 50, key = "order")
public void createOrder() { /* ... */ }

// 反射读取
Method m = clazz.getDeclaredMethod("createOrder");
RateLimit rl = m.getAnnotation(RateLimit.class);
System.out.println(rl.qps()); // 50

注解本身不干活

注解只是贴标签,真正让它生效的是读取它的代码(框架)。你写 @Transactional 没生效,往往不是注解没写对,而是代理没生效或方法调用没走代理(同类内部调用)。

三、性能:反射的代价与优化 ​

反射调用比直接调用慢,原因是:getMethod 每次要遍历方法表、参数要装箱成 Object[]、访问检查、以及无法被 JIT 内联。

优化手段:

java
// 1. 缓存 Method / Field 对象,别在循环里 getDeclaredMethod
private static final Method SET_NAME;
static {
    try { SET_NAME = User.class.getDeclaredMethod("setName", String.class); SET_NAME.setAccessible(true); }
    catch (Exception e) { throw new ExceptionInInitializerError(e); }
}

// 2. 极端性能场景:MethodHandle(JIT 友好)或字节码生成(ASM/ByteBuddy)
MethodHandle mh = MethodHandles.lookup().findVirtual(User.class, "setName",
        MethodType.methodType(void.class, String.class));
mh.invoke(u, "Tom");

框架为什么不怕反射慢

框架在启动时用反射扫描、解析、缓存元信息(方法句柄、映射关系),运行时尽量走缓存或生成字节码。所以启动慢、运行快是常态。

四、注解处理器(APT)——编译期生成代码 ​

@Retention(SOURCE) 的注解虽然运行期读不到,但可以在编译期被 javax.annotation.processing.Processor 读取并生成代码(Lombok、MapStruct、ButterKnife 都是这个套路)。

java
@SupportedAnnotationTypes("com.demo.Builder")
@SupportedSourceVersion(SourceVersion.RELEASE_17)
public class BuilderProcessor extends AbstractProcessor {
    @Override public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment env) {
        // 读取被 @Builder 标注的类,用 Filer 生成 XxxBuilder.java
        return true;
    }
}

五、最佳实践 ​

  • 优先用接口/多态,反射作为兜底:能用编译期类型解决的,不要上反射。
  • 缓存反射元数据:Method/Field/Constructor 只取一次,静态常量持有。
  • setAccessible(true) 要谨慎:破坏封装,且在 JPMS 模块化下可能被拒。
  • 注解属性只能是基本类型/String/Class/枚举/注解或其数组,不能是任意对象。
  • 自定义注解记得 @Retention(RUNTIME),否则反射读不到(这是最常见的坑)。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. User.class、obj.getClass()、Class.forName() 三者的区别是什么?哪个会触发静态代码块?
  2. 写一个自定义注解 @ExcelColumn(name="..."),用反射读取它并打印「字段名 → 列名」的映射。
  3. 为什么反射调用比直接调用慢?MethodHandle 为什么比 Method.invoke 快?
  4. 一个 @Transactional 标注的 private 方法为什么不生效?从 AOP 代理角度解释。

本节小结

反射的核心是运行时的 Class 对象 + 元数据读取 + 调用;注解的核心是保留策略——只有 RUNTIME 才能被反射读到,SOURCE 只能给 APT 用。框架「启动时反射扫描并缓存、运行时少反射」是性能关键。记住:注解只是标签,读它的代码才是灵魂。