Java 反射与动态代理:框架底层原理的一次讲透

CLARA轻量论坛系统
CLARA轻量论坛系统 星耀SVIP管理员 黑卡会员
发布于 2026-09-20 04:47 ·3 浏览 ·0 回复

学完这篇,你能用反射手写一个可运行的事务代理,并彻底搞清楚「为什么注入接口能成功、注入实现类会报错」这类框架经典问题。

第一步:先搞明白反射在干嘛

一句话:反射就是让程序在运行时才去认识一个类——拿它的构造器、方法、字段,并且调用它们。编译期写死的 `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 会动态生成一个字节码版本的访问器,速度接近直接调用。

所以真正要优化的只有两件事:

  1. 别在循环里反复查找。把 `Method`、`Field`、`Constructor` 缓存到 `static final` 或 `Map` 里,查找才是大头。
  2. 私有成员记得 `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` 类和方法不会被增强。
  • 代理对象不是实现类,注入时用接口类型最稳。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-541.html
转载请注明出处,版权归原作者所有。

全部回复 0

还没有回复,来抢沙发~