反射与注解
Java 中级 · Java 进阶
反射(Reflection)让程序在运行时检查、调用类与对象;注解(Annotation)是贴在代码上的元数据,两者结合构成了 Spring、MyBatis 等几乎所有框架的底层基础。理解它俩,才算真正看懂框架是怎么「反着控制」你的代码的。
一、反射的本质:Class 对象
JVM 加载一个类时,会在方法区创建一份类元信息,并在堆中生成一个 java.lang.Class 对象作为它的访问入口。反射的一切都从这个 Class 对象开始。
1.1 获取 Class 对象的三种方式
// 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 反射操作成员
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 自定义注解
@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 内联。
优化手段:
// 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 都是这个套路)。
@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),否则反射读不到(这是最常见的坑)。
📚 官方文档与参考资料
- Java SE 21 · java.lang.reflect 包
- Java 教程 · Reflection API
- Java SE 21 · java.lang.annotation
- Java 语言规范 · Annotations
✏️ 练习与自测
User.class、obj.getClass()、Class.forName()三者的区别是什么?哪个会触发静态代码块?- 写一个自定义注解
@ExcelColumn(name="..."),用反射读取它并打印「字段名 → 列名」的映射。 - 为什么反射调用比直接调用慢?
MethodHandle为什么比Method.invoke快? - 一个
@Transactional标注的 private 方法为什么不生效?从 AOP 代理角度解释。
本节小结
反射的核心是运行时的 Class 对象 + 元数据读取 + 调用;注解的核心是保留策略——只有 RUNTIME 才能被反射读到,SOURCE 只能给 APT 用。框架「启动时反射扫描并缓存、运行时少反射」是性能关键。记住:注解只是标签,读它的代码才是灵魂。