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 是面试必考
EXPLORER
📁 com.example
User.java
ReflectionDemo.java
// ==================== 反射核心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→注入依赖→注册路由→创建代理,全链路依赖反射
EXPLORER
📁 org.springframework
📁 beans.factory
📄 AbstractAutowireCapableBeanFactory.java
📄 AutowiredAnnotationBeanPostProcessor.java
// ====== 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拦截 → 反射调用目标方法
EXPLORER
📁 com.example.proxy
UserService.java
UserServiceImpl.java
JdkProxyDemo.java
// ====== 步骤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类
EXPLORER
📁 com.example.proxy
OrderService.java
CglibProxyDemo.java
// ====== 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方法❌ 不能❌ 不能
核心APIProxy + InvocationHandlerEnhancer + MethodInterceptor
调用目标方法method.invoke(target)proxy.invokeSuper(obj, args)
Spring Boot默认1.x 默认2.x 默认 (proxy-target-class=true)
性能JDK8+略优于CGLIBJDK8前优于JDK代理
生成类名$Proxy0XXX$$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对象,不会每次都查找,所以实际性能损失可控

EXPLORER
📁 com.example
ReflectionBenchmark.java
BadSingleton.java
// ====== 反射性能对比测试 ====== 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默认用CGLIBspring.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的"为什么不生效"问题都有答案。

📁 运行原理图解 / 11-Java反射与动态代理图解 — 配套阅读:① Java请求全链路 · ③ Spring运行真相
核心三原则速记

反射三原则:① 反射是 Spring IoC/AOP 的底层基石 — 运行时看穿类结构 → ② JDK Proxy 只能代理接口,CGLIB 代理子类(final 类/方法不行)→ ③ 反射性能比直接调用慢 3-10 倍,框架会用缓存 + 代码生成来优化