32 面试官问动态代理,其实在看你懂不懂“运行期凭空造类“

发布时间:2026/11/3 19:27:39
32 面试官问动态代理,其实在看你懂不懂“运行期凭空造类“ 面试现场代理这道题特别容易问出深浅。面试官Spring 的 AOP 你用过吧底层靠什么实现的候选人A动态代理。有接口的时候用 JDK 动态代理没接口的时候用 CGLIB。面试官那 JDK 动态代理生成的类长什么样你见过吗候选人A没见过……反正就是生成一个代理类。面试官它为什么要基于接口直接代理类不行吗候选人A卡住了。候选人B接上这个得从 Java 的单继承说起。JDK 动态代理生成的代理类自己已经extends java.lang.reflect.Proxy了一个类只能有一个父类剩下的我是谁只能靠implements接口来声明。所以它天生只能代理接口。想直接代理类就得换 CGLIB——它走的是生成子类这条路。面试官接着说说反射到底慢在哪。一、Class 对象类的户口本要理解反射先要理解Class这个类。每个类被 JVM 加载之后会在方法区JDK 8 之后叫元空间里生成一份类的元信息并且为这个类创建一个Class类型的对象用它来表示这个类本身。类的一切——方法、字段、构造器、注解、父类、接口——都挂在这个Class对象上。拿到Class对象有三条路Class? c1 String.class; // 类字面量编译期就确定不触发静态初始化 Class? c2 hello.getClass(); // 从对象反推 Class? c3 Class.forName(java.lang.String); // 运行期按全限定名加载会触发静态初始化这里有个面试常挖的坑String.class和Class.forName(...)都对区别在初始化时机。前者只加载、链接不跑静态代码块后者会触发静态初始化static {}会执行。用哪个取决于你要不要触发类的初始化。另外提一句基本类型也有 Class 对象int.class、int[].class都是合法的void.class也有。二、反射的三板斧日常反射就三件事找方法、调方法、造对象。Class? clazz Class.forName(com.example.User); // 1. 获取构造器 → 造对象 Constructor? ctor clazz.getDeclaredConstructor(String.class); ctor.setAccessible(true); // 关闭访问检查否则 private 构造器调不了 Object user ctor.newInstance(张三); // 2. 获取方法 → 调用 Method setName clazz.getMethod(setName, String.class); // 只找 public含继承 setName.invoke(user, 李四); // 3. 获取字段 → 读写 Field age clazz.getDeclaredField(age); age.setAccessible(true); age.setInt(user, 18);两个容易混的方法面试官十有八九会追问-getMethods()/getFields()拿所有 public 的包含从父类继承来的。-getDeclaredMethods()/getDeclaredFields()只拿本类自己声明的包括 private、protected不包括继承的。所以想拿父类的私有方法得先getSuperclass()一层层往上找。想碰私有成员必须setAccessible(true)否则抛IllegalAccessException。反射为什么慢这是必答题。原因有几层拆开说1.访问检查Method.invoke每次都要做访问权限校验setAccessible(true)能关掉这一层。2.参数装箱与数组封装invoke(Object obj, Object... args)的变长参数会把每次调用打包成一个Object[]基本类型还要装箱凭空多出一堆临时对象。3.方法查找getMethod是按名字和参数类型去遍历匹配本身就不便宜所以要缓存Method对象。4.难以内联直接调用编译成invokevirtualJIT 能把它内联进调用方反射调用是间接的JIT 在内联上更保守。需要说明的是JDK 对反射做了优化。当同一个Method被调用到一定次数inflation 阈值默认 15后会从本地实现切换成动态生成字节码实现sun.reflect.GeneratedMethodAccessor后续调用会快不少。所以反射慢很多的说法要加个前提——没预热、频繁调用的场景才明显。具体倍数跟 JVM 版本、预热程度都有关没有统一数字面试时别报一个拍脑袋的百分比。三、JDK 动态代理只能代理接口JDK 的动态代理核心就一个方法Object proxy Proxy.newProxyInstance( loader, // 类加载器 interfaces, // 要实现的接口数组 handler // 调用处理器 );用起来是这样interface UserService { void save(String name); } class UserServiceImpl implements UserService { public void save(String name) { System.out.println(保存 name); } } UserService target new UserServiceImpl(); UserService proxy (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), new Class[]{ UserService.class }, new InvocationHandler() { public Object invoke(Object p, Method method, Object[] args) throws Throwable { System.out.println(开始...); // 前置增强 Object result method.invoke(target, args); // 调用真实对象 System.out.println(结束...); // 后置增强 return result; } }); proxy.save(张三);编译器在运行期动态生成一个类通常叫$Proxy0编号会递增。它的结构大致是// $Proxy0 的等价示意 public final class $Proxy0 extends Proxy implements UserService { private static Method m3; // 对应 save 方法 public $Proxy0(InvocationHandler h) { super(h); } public final void save(String name) { try { super.h.invoke(this, m3, new Object[]{ name }); // 转交给 InvocationHandler } catch (Throwable e) { throw new UndeclaredThrowableException(e); } } }想亲眼看一眼这个生成的类可以让 JVM 把它存到磁盘上JDK 8 用-Dsun.misc.ProxyGenerator.saveGeneratedFilestrueJDK 9 之后属性名变成-Djdk.proxy.ProxyGenerator.saveGeneratedFilestrue。开启后运行程序会在工作目录的com/sun/proxy/下冒出$Proxy0.class之类的文件用javap一反编译源码结构看得清清楚楚。面试时能说出这个属性杀伤力不小。为什么必须基于接口看第一行——$Proxy0 extends Proxy。Java 是单继承这个名额被Proxy占了。那代理类怎么知道自己是UserService 的代理只能靠implements UserService。换句话说代理类跟目标类之间没有继承关系只能靠实现同一个接口来骗过调用方。这就是 JDK 动态代理天生绑定接口的原因。副作用也很直接没有接口的类JDK 代理爱莫能助。而且代理类只能调用接口里声明的方法目标类里接口之外的方法代理不认。四、CGLIB靠认爹也就是继承没有接口怎么办CGLIBCode Generation Library走的是另一条路——运行期生成目标类的子类覆写目标方法在覆写里插入增强逻辑。底层用 ASM 字节码框架直接拼字节码。Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserServiceImpl.class); // 设成父类生成的类是它的子类 enhancer.setCallback(new MethodInterceptor() { public Object intercept(Object obj, Method m, Object[] args, MethodProxy mp) throws Throwable { System.out.println(前置); Object r mp.invokeSuper(obj, args); // 调用父类真实方法 System.out.println(后置); return r; } }); UserServiceImpl proxy (UserServiceImpl) enhancer.create();它生成的类大致是这样// CGLIB 生成类的等价示意 public class UserServiceImpl$$EnhancerByCGLIB extends UserServiceImpl { public void save(String name) { // 拦截器链 → 最终调用 super.save(name) } }约束来自继承这两个字-不能代理final类——final 类没法被继承。-不能代理final方法——final 方法没法被覆写。-private方法拦不住——私有方法不参与多态子类覆写不了。-构造器会被调两次目标类的构造器 子类的构造器各一次子类初始化时会再调父类构造这是它的一个已知代价。五、对比与 Spring 的选择维度JDK 动态代理CGLIB原理生成类实现接口生成类继承目标类前提必须有接口类不能是 final拦截范围接口中声明的方法可覆写的非 final 方法依赖JDK 自带第三方字节码库Spring 已内嵌典型代表ProxyInvocationHandlerEnhancerMethodInterceptorSpring AOP 的选择- 老版本的策略是有接口用 JDK无接口用 CGLIB。- Spring Boot 2.x 开始默认spring.aop.proxy-target-classtrue也就是默认都用 CGLIB。原因是接口代理时注入方必须按接口类型注入如果某个 Bean 实际类型里有些方法没在接口里声明用 JDK 代理就会丢方法容易踩坑。统一用 CGLIB按具体类型注入更省心。- 顺带说一句Spring 用的 CGLIB 是重新打包进spring-core的改过包名所以你在项目里不一定能看到独立的 cglib 依赖。 面试官真正想听的答案问动态代理他想确认三件事你懂不懂Class、懂不懂运行期生成类、以及能不能把两种代理讲清楚为什么有这个限制。第一层给结论动态代理的本质是在运行期动态生成一个类用它对目标对象做一层包装。JDK 动态代理生成的是实现接口的类所以必须有接口CGLIB 生成的是继承目标类的子类所以目标类不能是 final、目标方法不能是 final。第二层讲机制Proxy.newProxyInstance生成的$Proxy0为什么只能implements接口——因为它已经extends Proxy单继承的名额被占了CGLIB 靠 ASM 拼字节码生成子类覆写方法插入增强。第三层讲反射的代价与优化慢在访问检查、装箱、方法查找、难以内联四点setAccessible(true)能省访问检查缓存Method能省查找而且 JDK 有 inflation 机制会生成GeneratedMethodAccessor加速。能点出性能依赖预热、不给绝对值比背一个倍数更靠谱。下篇预告反射是运行期操作类型还有一类新特性是把行为当参数传——Lambda 和 Stream。下一篇聊Stream 的惰性求值到底懒在哪、中间操作和终止操作的分界、map和flatMap怎么选、并行流为什么一不留神就出错共享可变状态 共用的 ForkJoinPool、以及Optional到底该怎么用才对。面试问什么时候用并行流答案绝对不是数据多就用。唠点键盘之外的 · 第 32 篇

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询