Java 反射与动态代理:框架底层原理的一次讲透
讲解 Java 反射的 Class 获取、实例创建、方法字段调用、public 与 declared 差异、setAccessible、缓存优化及 JDK 9 模块 opens 限制,为动态代理和框架注入接口与实现类差异打基础。
学完这篇,你能用反射手写一个可运行的事务代理,并彻底搞清楚「为什么注入接口能成功、注入实现类会报错」这类框架经典问题。
第一步:先搞明白反射在干嘛
一句话:反射就是让程序在运行时才去认识一个类——拿它的构造器、方法、字段,并且调用它们。编译期写死的 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类和方法不会被增强。 - 代理对象不是实现类,注入时用接口类型最稳。
转载请注明出处,版权归原作者所有。
正式会员
认证极客






