Java 反射与动态代理图解
Spring 的"魔法"全靠它 — 反射是X光透视,看穿对象内部一切;动态代理是替身演员,在你不知道的地方拦截方法调用
反射核心API
Spring如何用反射
JDK动态代理
CGLIB代理
性能与安全
面试FAQ
① 反射是什么?— X光透视
正常你只能用"门上的按钮"(public 方法),反射让你能打开任何门,包括 private 的。
🏥 类比:X光透视
正常看病:医生只能看到皮肤表面(public 方法/字段)
反射 = X光机 :戴上透视眼镜,骨骼、内脏、血管全看得到(private、protected 全部可见)
Spring = 医院主任 :拿着X光机给每个病人检查——发现 @Autowired 就帮你打针(注入依赖),发现 @RequestMapping 就帮你挂个号(注册路由)
没有反射 → Spring 就像个没有X光的医生,只知道"有个病人来了",但看不到他体内有什么注解、什么字段,就没办法自动帮你做事
一句话理解
正常调用 :你写 user.getName() → 编译器在编译时就知道 getName() 方法存在
反射调用 :你写 method.invoke(user) → 编译时不知道调用哪个方法,运行时 才通过字符串找到方法并调用
区别就在:编译时确定 vs 运行时确定 。反射把"硬编码"变成"动态发现"。
图1:正常调用 vs 反射调用
✋ 正常调用(编译时确定)
User user = new User()
user.getName()
✅ 编译器检查:getName()存在吗?方法签名对吗?
✅ 类型安全:参数类型不匹配 → 编译报错
🔍 反射调用(运行时发现)
Class<?> cls = Class.forName
("com.app.User")
Method m = cls
.getMethod("getName")
⚠️ 编译器不知道"getName"这个字符串对不对
⚠️ 拼错字符串 → 运行时NoSuchMethodException
图1:正常调用在编译时检查方法是否存在,反射调用在运行时才发现
PRO
面试中"反射是什么"的标准回答:反射是Java在运行时检查和操作类、方法、字段的能力 ,核心类是 java.lang.reflect 包下的 Class、Method、Field、Constructor。Spring 的 IoC 和 AOP 全部依赖反射实现。
② 反射核心API — 四个关键类
反射的入口永远是 Class 对象,从它能拿到 Method、Field、Constructor 三兄弟。
图2:反射API层次结构
Class<?>
类的元数据入口
Method
getName() / getReturnType()
invoke(obj, args) ← 核心!
getParameterTypes()
Field
getName() / getType()
get(obj) / set(obj, val)
setAccessible(true) ← 破private
Constructor
getParameterTypes()
newInstance(args) ← 创建对象
getDeclaredConstructors()
⚡ 面试必考区别:getDeclaredXxx vs getXxx
getDeclaredMethods() → 只看自己类声明的方法(含private)
getMethods() → 自己 + 父类的所有 public 方法(不含private)
图2:Class 是入口,Method/Field/Constructor 是三兄弟,getDeclaredXxx 和 getXxx 是面试必考
// ==================== 反射核心API完整演示 ====================
public class User {
private String name; // #1 private字段
public Integer age; // #2 public字段
public User (String name, Integer age) {
this .name = name; this .age = age;
}
public String getName () { return name; }
private void secret () { System.out.println("private method!" ); }
}
// ==================== 反射操作 ====================
Class <?> cls = Class .forName ("com.example.User" ); // ① 拿到Class对象
// --- 获取所有方法 ---
Method [] publicMethods = cls.getMethods (); // 只有public(含父类Object的)
Method [] allDeclared = cls.getDeclaredMethods (); // 含private,不含父类
// --- 调用private方法 ---
Method secret = cls.getDeclaredMethod ("secret" );
secret.setAccessible (true ); // ← 破private!
secret.invoke (userInstance); // 输出: private method!
// --- 访问private字段 ---
Field nameField = cls.getDeclaredField ("name" );
nameField.setAccessible (true ); // ← 破private!
String name = (String) nameField.get (userInstance); // 读private字段
nameField.set (userInstance, "NewName" ); // 改private字段!
// --- 通过构造器创建对象 ---
Constructor <?> ctor = cls.getConstructor (String.class , Integer .class );
Object user = ctor.newInstance ("Tom" , 25 ); // 等于 new User("Tom", 25)
💡 记忆口诀
getDeclared = 只看自己(含private) ,get = 自己+父类(只有public)
一个看深度(Declared能看到private),一个看广度(不带Declared能看到父类)。想要又深又广?两个都要调!
方法 范围 含private? 含父类?
getMethods() 所有public方法 ❌ ✅
getDeclaredMethods() 自己声明的方法 ✅ ❌
getFields() 所有public字段 ❌ ✅
getDeclaredFields() 自己声明的字段 ✅ ❌
getConstructors() 所有public构造器 ❌ —
getDeclaredConstructors() 所有构造器 ✅ —
③ Spring 如何用反射?— 透视眼镜实战
Spring 启动时戴上X光透视眼镜,扫描所有类的注解,自动帮你装配Bean、注入依赖、注册路由。
🏥 类比:医院主任查房
Spring启动 = 主任拿着X光机查房:
① 扫描 :看到所有病人(ClassPath扫描所有类)
② 发现注解 :这个病人贴了"需要打针"的标签(@Autowired)→ 帮他打针
③ 发现注解 :这个病人贴了"需要挂号"的标签(@RequestMapping)→ 帮他挂号
④ 发现注解 :这个病人贴了"是重点病人"的标签(@Service)→ 给他安排床位(创建Bean)
没有反射 → 主任只看到病人名字,看不到标签 → 什么自动化都做不了
图3:Spring启动时反射扫描全流程
① ClassPath扫描
遍历所有.class文件
→ Class<?> 对象
② 检查类注解
cls.getAnnotation()
@Service? @Component?
③ 反射创建实例
ctor.newInstance()
= new UserService()
④ 反射注入依赖
field.set(bean, dep)
@Autowired 自动注入
Spring 反射的 5 大应用场景
1. IoC创建Bean
Constructor
.newInstance()
替代 new 关键字
2. @Autowired注入
Field.setAccessible()
+ Field.set()
private字段也能注入
3. @RequestMapping
Method.getAnnotation()
+ Method.invoke()
请求路由到方法
4. AOP代理创建
JDK Proxy / CGLIB
底层都是反射
拦截方法调用
5. @Transactional
代理对象.invoke()
→ 开启事务
→ 执行真实方法
图3:Spring启动时用反射扫描注解→创建Bean→注入依赖→注册路由→创建代理,全链路依赖反射
// ====== Spring源码:反射创建Bean(简化版) ======
// 文件:AbstractAutowireCapableBeanFactory.java
protected Object createBeanInstance (String beanName, RootBeanDefinition mbd) {
Class <?> beanClass = resolveBeanClass (mbd, beanName); // ① 加载Class
// ② 反射找构造器
Constructor <?>[] ctors = beanClass.getDeclaredConstructors ();
// ③ 选一个构造器(自动注入模式选参数最多的)
Constructor <?> selected = selectConstructor (ctors);
// ④ 反射创建实例 = new UserService()
return selected.newInstance (); // ← 这里就是反射!
}
// ====== Spring源码:@Autowired反射注入(简化版) ======
// 文件:AutowiredAnnotationBeanPostProcessor.java
private void injectFields (Object bean, Class <?> clazz) {
// ① 反射获取所有字段(含private)
Field [] fields = clazz.getDeclaredFields ();
for (Field field : fields) {
// ② 检查有没有 @Autowired 注解
Autowired autowired = field.getAnnotation (Autowired .class );
if (autowired != null ) {
// ③ 破private!
field.setAccessible (true );
// ④ 从容器找到依赖的Bean,注入进去
Object dependency = getBean (field.getType());
field.set (bean, dependency); // ← 反射赋值!
}
}
}
PRO
面试必问:Spring为什么用反射而不用new? 因为Spring在编译时根本不知道你要创建什么类——类名是从配置文件/注解/包扫描中运行时 发现的。用反射,Spring可以处理任何类,不修改代码就能扩展。
④ JDK 动态代理 — 基于接口的替身
JDK动态代理是Java原生提供的代理机制,核心是 InvocationHandler + Proxy.newProxyInstance()。
🎭 类比:替身演员
你(目标对象)是主演,但拍危险戏时,导演(Spring)会请替身演员(代理对象)来演
观众(调用方)只看到"演员在演戏",不知道换人了
JDK替身 :只能替签了合同(实现了接口)的演员 → 演戏前先签协议(接口定义了能演什么戏)
CGLIB替身 :直接找演员的徒弟(子类)来替 → 不需要签合同,但final演员不能替
图4:JDK动态代理原理
调用方
调用方法
$Proxy0(代理对象)
实现相同的接口
method.invoke() →
handler.invoke()
委托
InvocationHandler
invoke(proxy, method, args)
→ @Before 前置逻辑
method.invoke(target)
目标对象
UserServiceImpl
真实业务逻辑
返回结果
→ @After 后置逻辑 → 返回给调用方
⚠️ 限制:目标类必须实现接口
没有接口 → JDK代理无法使用!
图4:JDK动态代理 = Proxy生成代理类 → InvocationHandler拦截 → 反射调用目标方法
// ====== 步骤1:定义接口(JDK代理必须有接口!)======
public interface UserService {
String getUser (Long id);
void deleteUser (Long id);
}
// ====== 步骤2:真实实现 ======
public class UserServiceImpl implements UserService {
public String getUser (Long id) {
return "User-" + id;
}
public void deleteUser (Long id) {
System.out.println("Deleted " + id);
}
}
// ====== 步骤3:InvocationHandler ======
public class LogInvocationHandler implements InvocationHandler {
private final Object target; // 真实对象
public LogInvocationHandler (Object target) {
this .target = target;
}
@Override
public Object invoke (Object proxy, Method method, Object [] args) throws Throwable {
// @Before — 前置逻辑
System.out.println("[LOG] 调用: " + method.getName());
// 真正调用目标方法(反射!)
Object result = method.invoke (target, args); // ← 反射!
// @After — 后置逻辑
System.out.println("[LOG] 返回: " + result);
return result;
}
}
// ====== 步骤4:创建代理并使用 ======
UserService real = new UserServiceImpl ();
UserService proxy = (UserService ) Proxy .newProxyInstance (
real.getClass().getClassLoader (), // 类加载器
real.getClass().getInterfaces (), // 接口列表 ← 关键!
new LogInvocationHandler (real) // Handler
);
// 调用方不知道自己用的是代理!
proxy.getUser (1L );
// 输出: [LOG] 调用: getUser
// User-1
// [LOG] 返回: User-1
⚠️ JDK代理的限制
必须实现接口 :如果 UserServiceImpl 没有实现 UserService 接口,Proxy.newProxyInstance() 会直接报错
不能代理类 :JDK代理生成的是接口的实现类,不是目标类的子类。所以只能通过接口类型来引用代理对象
不能代理final方法 :final方法不能被重写,代理无法拦截
⑤ CGLIB 代理 — 基于继承的替身
CGLIB通过生成目标类的子类来实现代理,不需要接口。Spring Boot 2.x 默认使用CGLIB。
🎭 类比续:徒弟替师
JDK替身 :演员签了合同(接口),替身也签同样的合同来演戏 → 观众看合同,不知道谁在演
CGLIB替身 :直接收个徒弟(子类),徒弟继承了师父所有技能,但每个动作前后可以加自己的花式 → 观众看到的是"师父在演",实际是徒弟
徒弟不能替final师父 (final类不能被继承)→ 这就是CGLIB不能代理final类的原因
图5:CGLIB代理原理(生成子类)
JDK 动态代理
接口 UserService
getUser() / deleteUser()
UserServiceImpl
$Proxy0
都实现接口,平行关系
✅ 接口+实现类 → 可以代理
❌ 没有接口的类 → 不能代理!
CGLIB 代理
UserServiceImpl(目标类)
不需要接口!
UserServiceImpl$$EnhancerByCGLIB
extends UserServiceImpl(子类!)
继承
✅ 有接口没接口都能代理
❌ final类/final方法 → 不能代理!
图5:JDK代理靠接口(平行),CGLIB靠继承(父子),这就是为什么CGLIB不能代理final类
// ====== CGLIB代理:不需要接口! ======
public class OrderService { // 注意:没有实现任何接口!
public String createOrder (String product) {
return "Order for: " + product;
}
}
// ====== MethodInterceptor(CGLIB的InvocationHandler)======
public class TransactionInterceptor implements MethodInterceptor {
@Override
public Object intercept (Object obj, Method method, Object [] args,
MethodProxy proxy) throws Throwable {
// @Before — 开启事务
System.out.println("[TX] 开始事务" );
try {
// 调用父类(真实对象)的方法
Object result = proxy.invokeSuper (obj, args); // ← 注意是invokeSuper!
// @AfterReturning — 提交事务
System.out.println("[TX] 提交事务" );
return result;
} catch (Exception e) {
// @AfterThrowing — 回滚事务
System.out.println("[TX] 回滚事务: " + e.getMessage());
throw e;
}
}
}
// ====== 创建CGLIB代理 ======
Enhancer enhancer = new Enhancer ();
enhancer.setSuperclass (OrderService .class ); // 设置父类(被代理类)
enhancer.setCallback (new TransactionInterceptor ()); // 设置拦截器
OrderService proxy = (OrderService ) enhancer.create (); // 创建代理子类
proxy.createOrder ("iPhone" );
// 输出: [TX] 开始事务
// Order for: iPhone
// [TX] 提交事务
对比项 JDK 动态代理 CGLIB 代理
实现方式 实现接口 继承目标类(生成子类)
是否需要接口 必须 不需要
能否代理final类 — ❌ 不能
能否代理final方法 ❌ 不能 ❌ 不能
能否代理private方法 ❌ 不能 ❌ 不能
核心API Proxy + InvocationHandler Enhancer + MethodInterceptor
调用目标方法 method.invoke(target) proxy.invokeSuper(obj, args)
Spring Boot默认 1.x 默认 2.x 默认 (proxy-target-class=true)
性能 JDK8+略优于CGLIB JDK8前优于JDK代理
生成类名 $Proxy0 XXX$$EnhancerByCGLIB$$xxx
PRO
面试速答:"JDK代理靠接口,CGLIB靠继承" 。JDK代理生成接口的实现类,CGLIB生成目标类的子类。Spring Boot 2.x默认用CGLIB,因为大部分Service没有接口。面试官问区别,说这句话+final类限制就够了。
⑥ 反射的坑与性能 — X光也有副作用
反射很强大,但有性能开销、安全风险、和Java 9+模块系统的限制。
🏥 类比续:X光的副作用
X光很强大(能看到骨骼),但:
性能问题 = X光机启动慢:每次照射都要调机器参数(反射每次调用都要查找方法、检查权限)
安全问题 = 看到不该看的:能看到病人的隐私部位(private字段/方法被暴露)
模块系统 = 医院装了新门禁:Java 9+模块系统限制了你乱照X光(--add-opens)
⚡ 性能:反射比直接调用慢多少?
反射调用大约比直接调用慢 2~50倍 ,取决于JDK版本和优化方式:
• Method.invoke() :JDK8+有Inflation优化,第16次调用后自动生成字节码,速度接近直接调用
• setAccessible(true) :每次调用都有安全检查开销
• Spring的优化 :Spring缓存了Method对象,不会每次都查找,所以实际性能损失可控
// ====== 反射性能对比测试 ======
public class ReflectionBenchmark {
static class Target {
public String hello (String name) { return "Hi, " + name; }
}
public static void main (String[] args) throws Exception {
Target target = new Target ();
Method method = Target .class .getMethod ("hello" , String.class );
int iterations = 10_000_000 ;
// ① 直接调用
long start = System.nanoTime ();
for (int i = 0 ; i < iterations; i++) {
target.hello ("World" );
}
System.out.println("直接调用: " + (System.nanoTime () - start) / 1_000_000 + "ms" );
// ② 反射调用(每次查找Method)
start = System.nanoTime ();
for (int i = 0 ; i < iterations; i++) {
Method m = Target .class .getMethod ("hello" , String.class );
m.invoke (target, "World" );
}
System.out.println("反射(不缓存): " + (System.nanoTime () - start) / 1_000_000 + "ms" );
// ③ 反射调用(缓存Method)
start = System.nanoTime ();
for (int i = 0 ; i < iterations; i++) {
method.invoke (target, "World" );
}
System.out.println("反射(缓存Method): " + (System.nanoTime () - start) / 1_000_000 + "ms" );
}
}
// 典型结果(JDK 17, 1000万次调用):
// 直接调用: ~30ms
// 反射(不缓存): ~8000ms ← 266倍慢!查找Method是大头
// 反射(缓存Method): ~60ms ← 只慢2倍,可接受
// ====== ❌ 反射破坏单例(面试高频陷阱!)======
public class Singleton {
private static volatile Singleton instance;
private Singleton () {
if (instance != null ) {
throw new RuntimeException ("Use getInstance()!" ); // ← 防反射
}
}
public static Singleton getInstance () {
if (instance == null ) { // DCL
synchronized (Singleton .class ) {
if (instance == null ) instance = new Singleton ();
}
}
return instance;
}
}
// 反射破解单例:
Constructor <?> ctor = Singleton .class .getDeclaredConstructor ();
ctor.setAccessible (true ); // 破private构造器!
Singleton fake = (Singleton ) ctor.newInstance (); // 💥 创建了第二个实例!
// 防御:枚举单例(反射无法创建枚举实例)
public enum SafeSingleton {
INSTANCE; // ✅ 反射.newInstance()会抛IllegalArgumentException
}
⚠️ Java 9+ 模块系统限制
Java 9引入了模块系统(JPMS) ,即使 setAccessible(true) 也可能失败:
• 模块默认不 open 包给其他模块 → InaccessibleObjectException
• Spring Boot 3.x(基于Spring 6)需要 --add-opens JVM参数才能正常工作
• 常见启动参数:--add-opens java.base/java.lang=ALL-UNNAMED
这就是为什么升级到Java 17后,一些反射代码会莫名其妙报错
🔒 反射安全三原则
1. 能不用就不用 :反射是最后的手段,99%的场景都有更好的替代方案
2. 缓存Method/Field :查找是最大的性能开销,Spring的做法是启动时查一次,之后复用
3. 警惕setAccessible :它能破private,但会破坏封装性。生产环境要考虑SecurityManager
PRO
面试速答"反射性能":"反射主要慢在查找Method,不在invoke本身。缓存Method后只慢2倍左右。Spring启动时缓存所有Method,所以运行时几乎无性能损失。" 这样说体现了你理解本质而不是背结论。
⑦ 面试 FAQ — 高频问题速答
点击问题展开答案,这些是面试中出现频率最高的反射相关问题。
Q1:什么是反射?哪些场景会用到?
定义 :反射是Java在运行时检查和操作类、方法、字段的能力,核心API在 java.lang.reflect 包。
场景 :① Spring IoC(反射创建Bean、注入依赖)② Spring AOP(JDK Proxy/CGLIB动态代理)③ 序列化框架(Jackson/Gson通过反射读写字段)④ ORM框架(MyBatis/Hibernate通过反射映射结果集)⑤ 测试框架(JUnit通过反射调用@Test方法)⑥ 注解处理(Spring扫描@Component等注解)
Q2:getDeclaredMethods() 和 getMethods() 的区别?
getDeclaredMethods() :只看当前类自己声明的方法,包含private ,不包含父类方法。
getMethods() :看当前类+所有父类的public方法 ,不含private。
口诀:Declared = 深(看private)不看广(不看父类) ,不带Declared = 广(看父类)不看深(不看private)。
Q3:JDK动态代理和CGLIB的区别?Spring Boot用的哪个?
JDK代理 :基于接口,Proxy.newProxyInstance()生成接口实现类。目标类必须实现接口。
CGLIB代理 :基于继承,Enhancer生成目标类子类。不需要接口,但不能代理final类/final方法。
Spring Boot 2.x默认用CGLIB (spring.aop.proxy-target-class=true ),因为大部分Service没有接口。1.x默认用JDK代理。
Q4:反射为什么慢?怎么优化?
慢的原因 :① 查找Method/Field需要遍历(O(n)) ② 每次invoke要做安全检查和参数校验 ③ JIT难以内联反射调用
优化方式 :① 缓存Method/Field对象 (Spring就是这么做的,查找一次复用多次) ② JDK8+有Inflation优化(第16次调用后生成字节码) ③ 用MethodHandle(Java 7+)替代反射,性能接近直接调用
关键点:慢在查找,不在invoke本身 。缓存Method后只慢约2倍,可以接受。
Q5:反射能破坏单例吗?怎么防止?
能! 反射通过 setAccessible(true) 可以调用private构造器,创建第二个实例。
防御1 :在private构造器中加检查,如果instance已存在则抛异常
防御2 (最佳):使用枚举单例 。Constructor.newInstance() 对枚举类会抛 IllegalArgumentException("Cannot reflectively create enum objects") ,这是JVM层面的保护
Effective Java推荐:单例模式的最佳实现是枚举 ,天然防反射、防序列化破坏
Q6:Spring为什么用反射而不是直接new?
因为Spring在编译时不知道要创建什么类 。类名来自:① 包扫描(@ComponentScan)② 配置文件(application.yml)③ 注解值(@Qualifier)
这些信息只有运行时 才能获取,必须用反射来动态创建。如果用new,Spring的代码就得硬编码每一个类名,完全失去IoC的意义。
Q7:setAccessible(true) 在Java 9+还管用吗?
不一定! Java 9引入了模块系统(JPMS),如果模块没有 open 包,setAccessible(true) 会抛 InaccessibleObjectException 。
解决方式 :启动时加JVM参数 --add-opens 模块名/包名=ALL-UNNAMED ,这就是Spring Boot 3.x启动时经常看到一堆--add-opens参数的原因。
这也是Java生态向模块化迁移的阵痛期——反射太自由了,模块系统要收一收。
Q8:CGLIB为什么不能代理final类和方法?
CGLIB通过生成子类 来实现代理:子类重写父类方法,在重写的方法中加入增强逻辑。
final类 :不能被继承 → 无法生成子类 → 无法代理
final方法 :不能被重写 → 子类无法覆盖此方法 → 此方法无法被拦截
这就是为什么Spring的Service类不要加final修饰符 ,否则AOP事务都不生效。
Q9:反射和MethodHandle有什么区别?
反射 :运行时通过字符串查找方法(getMethod("getName") ),每次调用都要查找+权限检查
MethodHandle :Java 7+引入,编译时类型安全,查找一次后可以像直接调用一样快。被称为"现代反射"
区别:反射是解释执行,MethodHandle是编译执行 。MethodHandle的性能更接近直接调用,但API更复杂
实际中:Spring和大多数框架还是用反射 ,因为生态成熟、API简单。MethodHandle主要用于JVM内部和高性能框架
Q10:@Transactional失效和反射/代理有什么关系?
@Transactional依赖AOP代理 。代理通过反射拦截方法调用,在方法前后加事务逻辑。
失效场景1:内部调用 :this.method() 不经过代理(this是目标对象,不是代理对象)→ 反射拦截不到
失效场景2:final/static方法 :CGLIB无法重写final方法 → 代理无法拦截 → 事务不生效
失效场景3:private方法 :代理只能拦截public/protected方法,private方法外部不可见
本质:代理是通过反射拦截方法调用的,如果你的调用绕过了代理,所有AOP功能都不生效
🎯 心智模型总结
反射 = X光透视 :运行时看穿对象内部,Spring用它来扫描注解、创建Bean、注入依赖。没有反射 → Spring就没有"魔法"。
动态代理 = 替身演员 :JDK替身需要接口(签合同),CGLIB替身靠继承(收徒弟)。Spring Boot 2.x默认用CGLIB。
核心关系链 :反射 → 动态代理 → AOP → @Transactional/Spring全部增强功能。理解了这条链,Spring的"为什么不生效"问题都有答案。