Java 反射与动态代理:框架底层原理的一次讲透
学完这篇,你能用反射手写一个可运行的事务代理,并彻底搞清楚「为什么注入接口能成功、注入实现类会报错」这类框架经典问题。
第一步:先搞明白反射在干嘛
一句话:反射就是让程序在运行时才去认识一个类——拿它的构造器、方法、字段,并且调用它们。编译期写死的 `new User()` 叫静态,反射是「先拿到说明书,再决定怎么用」。
拿到 `Class` 对象只有三条路:
Class<?> c1 = User.class; // 类字面量,编译期已知
Class<?> c2 = new User().getClass(); // 从实例拿
Class<?> c3 = Class.forName("com.demo.User"); // 从全限定名加载
注意:一个类在同一个类加载器下只有一个 `Class` 对象,所以 `c1 == c2 == c3` 是 `true`。但不同类加载器加载同名类,`Class` 就不相等,这是热部署、Tomcat 多应用场景下各种诡异报错的根源。
第二步:反射创建对象、调方法、改字段
// 1. 创建实例
Constructor<?> ctor = User.class.getDeclaredConstructor(String.class, int.class);
User u = (User) ctor.newInstance("阿乐", 18);
// 2. 调方法
Method m = User.class.getMethod("setName", String.class);
m.invoke(u, "新名字");
// 3. 改字段(含私有)
Field f = User.class.getDeclaredField("age");
f.setAccessible(true);
f.setInt(u, 20);
这里有个必须背下来的区别:
- `getMethod` / `getConstructor` / `getField`:只拿 public,而且包含父类继承来的。
- `getDeclaredXxx`:拿本类声明的全部(含 private、protected),但不含继承。
注意:`newInstance()`(Class 上的老方法)只能调无参 public 构造器,已经过时;用 `getDeclaredConstructor(...).newInstance(...)`。另外 `invoke` 抛出的异常会被包一层 `InvocationTargetException`,真正原因要 `e.getCause()` 拿,直接打日志只会看到一层壳。
第三步:`setAccessible(true)` 和性能那点事
反射慢是老黄历的一半真相。JDK 7 之后有「膨胀」机制:同一个方法反射调用超过阈值(默认 15 次),JDK 会动态生成一个字节码版本的访问器,速度接近直接调用。
所以真正要优化的只有两件事:
- 别在循环里反复查找。把 `Method`、`Field`、`Constructor` 缓存到 `static final` 或 `Map` 里,查找才是大头。
- 私有成员记得 `setAccessible(true)`,否则每次调用都走安全检查。
注意:JDK 9 引入模块系统后,反射进别人的包(比如 `java.base` 内部类)会因为 `opens` 限制直接抛 `InaccessibleObjectException`。你自己的业务类没这问题,动 JDK 内部类才需要 `--add-opens`。
第四步:JDK 动态代理——手写一个迷你 AOP
动态代理 = 反射 + 运行时代码生成。它能在不碰目标类源码的前提下,在方法前后插入逻辑(事务、日志、鉴权)。
先定接口和实现:
public interface OrderService { void create(String id); }
public class OrderServiceImpl implements OrderService {
public void create(String id) { System.out.println("下单 " + id); }
}
写处理器:
public class TxHandler implements InvocationHandler {
private final Object target;
public TxHandler(Object target) { this.target = target; }
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[开启事务]");
try {
Object r = method.invoke(target, args); // 关键:传 target
System.out.println("[提交事务]");
return r;
} catch (Exception e) {
System.out.println("[回滚]");
throw e;
}
}
}
生成代理并调用:
OrderService proxy = (OrderService) Proxy.newProxyInstance(
OrderService.class.getClassLoader(),
new Class[]{ OrderService.class },
new TxHandler(new OrderServiceImpl()));
proxy.create("A1001");
// 输出:开启事务 / 下单 A1001 / 提交事务
注意:`invoke` 里必须 `method.invoke(target, args)`。写成 `method.invoke(proxy, args)` 会自己调自己,直接 `StackOverflowError`。这是新手最常见的翻车点。
第五步:为什么注入实现类会失败
JDK 动态代理生成的 `$Proxy0` 是接口的实现类,不是 `OrderServiceImpl` 的子类。
System.out.println(proxy instanceof OrderService); // true
System.out.println(proxy instanceof OrderServiceImpl); // false
所以 Spring 里如果目标类有接口,代理对象只能按接口类型注入:
@Autowired private OrderService orderService; // ✅
@Autowired private OrderServiceImpl orderServiceImpl; // ❌ 可能 NoSuchBeanDefinition
要代理类本身,得用 CGLIB——它用 ASM 生成目标类的子类并覆写方法。
注意:CGLIB 代理不了 `final` 类和 `final` 方法。Spring Boot 2.x 起 `spring.aop.proxy-target-class` 默认为 `true`,即默认走 CGLIB;Spring Boot 1.x 默认是 JDK 代理。这也解释了为什么升级后某些注入写法会突然报错。
小结
- 反射三入口:`类.class`、`getClass()`、`Class.forName()`;查找方法用 `getDeclaredXxx` 才能拿到私有成员。
- 反射慢在「查找」不在「调用」,缓存 `Method`/`Field` 就能解决大部分性能问题。
- JDK 动态代理三件套:接口 + `InvocationHandler` + `Proxy.newProxyInstance`;只能代理接口。
- `invoke` 里务必传 `target`,异常记得看 `getCause()`。
- Spring AOP 默认 CGLIB(Boot 2.x 起),所以 `final` 类和方法不会被增强。
- 代理对象不是实现类,注入时用接口类型最稳。
转载请注明出处,版权归原作者所有。
星耀SVIP
管理员
黑卡会员





