⭐ 推荐:社区规则条款 V1.0

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

不语
不语 正式会员正式会员认证极客认证极客
发布于 2026-08-02 22:26 ·74 浏览 ·0 回复
内容摘要

讲解 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 会动态生成一个字节码版本的访问器,速度接近直接调用。

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

  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

还没有回复,来抢沙发~