反射与注解
1. 这是什么
反射允许程序在运行时获取类、方法、字段、构造器等元信息,并动态操作对象。
注解则为程序提供结构化元数据,常常配合反射参与框架运行。
一句话理解:
- 反射解决的是“运行时怎么知道对象长什么样、能做什么”
- 注解解决的是“怎么把额外规则、标记、配置挂到代码上”
2. 为什么重要
Spring、MyBatis、JUnit、Jackson 等大量框架能力,背后都依赖反射和注解。
理解它们,是进入 Java 框架世界的重要前提。
很多“看上去像魔法”的能力,本质上都是:
- 先通过反射拿到类和方法信息
- 再读取注解上的配置
- 最后按规则执行增强逻辑
3. 先建立直觉:反射和注解分别负责什么
可以把它们理解成两个角色:
- 反射:负责“看”和“调”
- 注解:负责“标”
举个框架感知很强的抽象过程:
- 某个类上的方法标了
@GetMapping("/users") - 框架启动时扫描这个类
- 通过反射找到这个方法
- 通过反射读取注解中的
"/users" - 把 URL 和方法建立映射关系
所以:
- 注解本身不会执行逻辑
- 真正执行逻辑的是“注解解析器”
- 反射通常是解析器的基础能力
4. 核心内容
4.1 Class 对象是什么
在 Java 里,每个类在运行时都有一个对应的 Class 对象,里面保存了类的元信息,例如:
- 类名
- 包名
- 父类
- 接口
- 字段
- 方法
- 构造器
- 注解
你可以把 Class 理解成“运行时的类说明书”。
4.2 获取 Class 对象的常见方式
最常见有三种:
UserService.class
obj.getClass()
Class.forName("com.example.UserService")适用场景:
类名.class:编译期已知类型,最直接obj.getClass():已经有对象实例Class.forName(...):只知道类的全限定名,常见于框架、插件、配置化加载
4.3 反射能做哪些事
常见能力包括:
- 读取字段信息
- 调用方法
- 调用构造器创建对象
- 判断类型关系
- 读取注解
示意代码:
Class<?> clazz = UserService.class;
Field field = clazz.getDeclaredField("name");
Method method = clazz.getDeclaredMethod("hello");
Constructor<?> constructor = clazz.getDeclaredConstructor(String.class);4.4 getField 和 getDeclaredField 的区别
这是非常常见的细节:
getField:只能拿到public字段,且会查父类getDeclaredField:拿当前类自己声明的字段,不限访问修饰符
方法和构造器也有类似区别:
getMethod/getDeclaredMethodgetConstructor/getDeclaredConstructor
4.5 为什么有时要 setAccessible(true)
如果你想访问:
private字段private方法private构造器
通常需要:
field.setAccessible(true);
method.setAccessible(true);这意味着:
- 你在绕过正常封装边界
- 它很强大,但也要谨慎使用
在较新的 Java 环境和模块系统下,反射访问还可能受到额外限制。
4.6 注解怎么定义
定义注解本质上是定义一个特殊接口:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Route {
String path();
}关键点有两个:
@Target:这个注解能标在哪@Retention:这个注解能保留到什么时候
4.7 注解保留策略为什么重要
常见保留策略有三种:
SOURCE:只在源码中存在,编译后丢弃CLASS:保留到字节码,但运行时默认反射读不到RUNTIME:运行时还能通过反射读取
如果你的目标是:
- 启动时扫描注解
- 运行时通过反射解析注解
那必须使用:
@Retention(RetentionPolicy.RUNTIME)4.8 注解本身不执行逻辑
这一点特别容易误解。
注解只是元数据,它不会自己“生效”。
例如你写了:
@Audit
public void save() {}如果没有解析器去扫描 @Audit 并执行增强逻辑,那么这行注解本身什么都不会做。
4.9 反射的价值和代价
反射的价值:
- 灵活
- 动态
- 适合框架扩展、插件机制、通用工具
反射的代价:
- 代码可读性变差
- 类型检查从编译期后移到运行期
- 性能通常不如直接调用
- 容易破坏封装
工程上的建议是:
- 框架层、基础设施层适度使用
- 业务代码里不要为“炫技”而过度依赖反射
5. 学习重点
这一章最重要的不是记 API,而是理解:
Class是运行时元信息入口- 反射是“运行时动态访问”的工具
- 注解是元数据,不是执行器
- 要让注解在运行时可读,必须用
RetentionPolicy.RUNTIME - 反射和注解经常一起出现,但职责不同
6. 常见问题
6.1 把注解当成“自动生效”的魔法
很多初学者以为只要写了注解就会生效。
其实真正执行的是:
- 框架
- 注解处理器
- 反射扫描逻辑
6.2 不理解保留策略导致读取不到
如果注解不是 RUNTIME,你在运行时通过反射大概率读不到。
6.3 过度依赖反射而忽视可维护性
反射很强,但过量使用会让代码:
- 跳转困难
- 调试困难
- 错误延迟到运行时
6.4 混淆 getMethod 和 getDeclaredMethod
这会导致你:
- 明明方法存在,却拿不到
- 或者只能拿到
public方法,读不到私有方法
7. 动手验证
这一节可以直接复制运行,边看边验证。
7.1 准备一个可运行示例
新建文件 ReflectionAnnotationDemo.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 编译并运行
javac ReflectionAnnotationDemo.java
java ReflectionAnnotationDemo如果你在 PowerShell 中操作,也直接执行同样两条命令即可。
7.3 你应该观察到什么
输出不一定逐字完全一致,但应包含这些关键信息:
sameClassObject=true
constructedClass=UserService
fieldValue=ChangedByReflection
privateMethodResult=hi, ChangedByReflection
routePath=/users
routeAnnotationPresent=true
classOnlyMarkerPresent=false7.4 每一行在验证什么
sameClassObject=true:说明三种获取Class的方式最终拿到的是同一个运行时类对象constructedClass=UserService:说明可以通过反射调用构造器创建对象fieldValue=ChangedByReflection:说明可以通过反射访问并修改私有字段privateMethodResult=hi, ChangedByReflection:说明可以通过反射调用私有方法routePath=/users:说明运行时注解可以被反射读取routeAnnotationPresent=true:说明RetentionPolicy.RUNTIME的注解在运行时确实可见classOnlyMarkerPresent=false:说明RetentionPolicy.CLASS的注解默认不能通过运行时反射读取
7.5 再做两个延伸验证
你可以继续做下面两个实验:
- 去掉
field.setAccessible(true)或method.setAccessible(true) - 把
@Retention(RetentionPolicy.RUNTIME)改成CLASS
你可以观察:
- 不开放访问时,私有成员无法直接反射访问
- 注解不是
RUNTIME时,运行时反射就读不到它
8. 练习建议
下面这些练习做完,这一章会更扎实:
- 自定义一个注解,并在运行时扫描类上的所有标注方法
- 用反射调用对象方法并设置字段值
- 对比
getMethod和getDeclaredMethod的行为差异 - 写一个简单的“注解驱动路由注册” demo
- 总结反射和注解在常见框架中的应用场景
9. 自测问题
Class对象能提供哪些运行时信息?getMethod和getDeclaredMethod有什么区别?- 注解为什么通常要配合反射使用?
- 注解保留策略对运行时行为有什么影响?
- 为什么说注解本身不执行逻辑?
10. 自测核对要点
如果你的回答能覆盖下面这些点,说明这一章基本掌握到位了:
Class是反射入口,提供类的元信息- 反射可以读取字段、调用方法、调用构造器、读取注解
- 注解是元数据,不是逻辑执行器
- 运行时读取注解通常要求
RetentionPolicy.RUNTIME - 反射很强大,但会带来性能、封装和可维护性代价