在日常开发中,你是否遇到过这样的场景:想给Service层的核心业务方法加日志,却不想在每个方法里重复写log.info(...);想给支付接口加权限校验,又怕修改原有逻辑引入bug;甚至想在不改动老代码的前提下,悄悄给旧系统加个性能监控?这些“不侵入原有代码的前提下给业务附加功能”的需求,其实都在呼唤同一个解决方案——代理模式(Proxy Pattern) 。本文将通过 my助手ai 的视角,从痛点切入到原理剖析,再到代码实战和面试要点,帮你彻底吃透这个面试必考、开发必用的核心知识点。
一、痛点切入:为什么需要代理?
先看一个传统写法。假设要给TaskService的方法加执行时间统计:
public class TaskService {public void dealTask(String taskName) { long startTime = System.currentTimeMillis(); // 统计逻辑 System.out.println("执行任务:" + taskName); // 核心业务 long cost = System.currentTimeMillis() - startTime; // 统计逻辑 System.out.println("耗时:" + cost + "ms"); } }
痛点一目了然:
耦合严重:统计逻辑与业务逻辑混在一起,修改统计规则就要改业务代码-64
代码冗余:每个需要统计的方法都要重复写时间记录代码
职责混乱:一个方法同时负责“业务执行”和“性能统计”,违背单一职责原则
代理模式的解决方案就是引入“中间人”——代理对象负责附加功能,目标对象专注核心业务。这样一来,附加功能与业务逻辑彻底解耦-64。
二、核心概念:什么是代理模式?
代理模式(Proxy Pattern) 是一种结构型设计模式,通过引入一个代理对象来控制对原始对象的访问,在不修改原始类代码的前提下增强其功能-11。
生活化类比:明星和经纪人的关系。明星只负责演戏(核心业务),经纪人负责接洽、合同、公关等杂事(附加功能)。外部只接触经纪人,不直接接触明星。
代理模式的三要素:
| 角色 | 说明 |
|---|---|
| 抽象主题(Subject) | 接口,定义目标对象和代理对象的共同行为 |
| 真实主题(RealSubject) | 目标对象,实现核心业务逻辑 |
| 代理类(Proxy) | 持有目标对象引用,负责附加功能 |
三、静态代理:最简单的“专属中间人”
标准定义
静态代理(Static Proxy) 的代理类代码在项目编译阶段就已生成,与目标类的字节码一同存在于最终产物中,需手动编写代理类-2。
代码示例
// 1. 抽象接口 public interface UserService { void addUser(String username); void deleteUser(String username); } // 2. 目标类:专注核心业务 public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("数据库新增用户:" + username); } @Override public void deleteUser(String username) { System.out.println("数据库删除用户:" + username); } } // 3. 静态代理类:手动编写,附加日志功能 public class UserServiceProxy implements UserService { private final UserService target; public UserServiceProxy(UserService target) { this.target = target; } @Override public void addUser(String username) { System.out.println("日志:开始执行addUser,参数:" + username); // 前置增强 target.addUser(username); System.out.println("日志:addUser执行完毕"); // 后置增强 } @Override public void deleteUser(String username) { System.out.println("日志:开始执行deleteUser,参数:" + username); target.deleteUser(username); System.out.println("日志:deleteUser执行完毕"); } }
优缺点分析
| 优点 | 缺点 |
|---|---|
| 符合开闭原则,不修改目标对象 | 类爆炸:每个目标类都需要对应代理类-11 |
| 编译期类型安全 | 接口耦合:接口新增方法,代理类必须同步修改 |
| 职责清晰,辅助逻辑集中在代理类 | 代码冗余,维护成本高 |
一句话总结:静态代理简单直接,但“一个萝卜一个坑”,代码量翻倍。
四、动态代理:一劳永逸的“万能中间人”
标准定义
动态代理(Dynamic Proxy) 的代理类并非提前编译好,而是在Java程序运行过程中,根据实际业务需求动态创建并加载,无需手动编写代理类代码-2。
概念对比
| 对比维度 | 静态代理 | 动态代理 |
|---|---|---|
| 生成时机 | 编译期 | 运行期 |
| 是否需要手写代理类 | 需要 | 不需要 |
| 代码量 | 每个目标类一个代理类 | 一个代理工厂服务所有类 |
| 灵活性 | 差 | 高 |
一句话概括两者关系:静态代理是“思想”(谁来代理),动态代理是“实现手段”(如何生成代理)。静态代理手动编码实现代理模式的思想;动态代理在运行期自动生成代理类,是实现代理模式的技术手段-1。
五、JDK动态代理(基于接口)
核心机制
JDK动态代理基于Java反射机制,核心依赖java.lang.reflect.Proxy类和InvocationHandler接口-22。运行时通过Proxy.newProxyInstance()动态生成代理类,代理类实现传入的接口,并将方法调用统一转发给InvocationHandler的invoke()方法-22。
代码示例
// 1. 业务接口(必须!JDK动态代理要求目标类实现接口) public interface UserService { void addUser(String username); } public class UserServiceImpl implements UserService { @Override public void addUser(String username) { System.out.println("新增用户:" + username); } } // 2. 实现InvocationHandler,定义增强逻辑 public class LogInvocationHandler implements InvocationHandler { private final Object target; public LogInvocationHandler(Object target) { this.target = target; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("日志:" + method.getName() + "方法开始执行"); // 前置增强 Object result = method.invoke(target, args); // 反射调用 System.out.println("日志:" + method.getName() + "方法执行结束"); // 后置增强 return result; } } // 3. 动态生成代理对象 UserService target = new UserServiceImpl(); UserService proxy = (UserService) Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), new LogInvocationHandler(target) ); proxy.addUser("张三");
底层支撑
JDK动态代理的底层依赖 Java反射机制。method.invoke(target, args)本质上是反射调用目标方法,这也是其性能损耗的主要来源-。
六、CGLIB动态代理(基于继承)
核心机制
CGLIB(Code Generation Library) 通过ASM字节码操作框架,在运行时动态生成目标类的子类,并覆盖所有非final方法,将方法调用拦截到MethodInterceptor.intercept()方法-29。它不需要目标类实现任何接口。
代码示例
// 1. 目标类:普通类,无需实现接口 public class UserService { public void addUser(String username) { System.out.println("新增用户:" + username); } } // 2. 实现MethodInterceptor,定义增强逻辑 public class LogMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println("日志:" + method.getName() + "开始执行"); Object result = proxy.invokeSuper(obj, args); // 调用父类原始方法 System.out.println("日志:" + method.getName() + "执行结束"); return result; } } // 3. 通过Enhancer生成代理对象 Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new LogMethodInterceptor()); UserService proxy = (UserService) enhancer.create(); proxy.addUser("张三");
底层支撑
CGLIB底层依赖 ASM字节码操作框架,在运行时动态生成字节码创建子类。与JDK动态代理不同,CGLIB通过FastClass机制避免了反射调用,早期版本性能优势明显-32。
七、JDK vs CGLIB:一张表吃透区别
| 对比维度 | JDK动态代理 | CGLIB动态代理 |
|---|---|---|
| 代理方式 | 基于接口 | 基于继承(生成子类) |
| 目标类要求 | 必须实现接口 | 无需接口,但不能是final类 |
| 底层技术 | 反射 + Proxy | ASM字节码增强 |
| 能否代理final方法 | 不涉及 | 不能(final方法无法被子类覆盖) |
| 性能(JDK 8以后) | 反射优化后差距缩小 | 调用效率略高 |
| 依赖 | Java标准库,无需额外引入 | 需引入CGLIB库(Spring已内置) |
| 典型场景 | Spring AOP代理有接口的Bean | Spring AOP代理无接口的Bean、Hibernate懒加载 |
性能小贴士:JDK 6及以前,CGLIB调用性能明显优于JDK;JDK 7开始JDK动态代理性能大幅提升,与CGLIB差距缩小;JDK 8以后两者差距已不显著-47。
八、高频面试题
Q1:代理模式和装饰器模式有什么区别?
答题要点:区分设计目的和增强方式。
设计目的不同:代理模式重在控制访问(如权限校验、懒加载),装饰器模式重在动态扩展功能(如IO流嵌套包装)-38
创建时机不同:代理模式通常由框架在运行时创建,装饰器模式由客户端主动包装
Q2:静态代理和动态代理有什么区别?为什么实际开发更倾向用动态代理?
答题要点:
生成时机:静态代理编译期确定,动态代理运行期生成-1
代码量:静态代理需为每个目标类手写代理类,动态代理一个代理工厂服务所有
维护性:接口新增方法时,静态代理的代理类必须同步修改;动态代理完全不需要改动
实际开发选择动态代理的原因:避免类爆炸、降低维护成本、提高灵活性
Q3:JDK动态代理和CGLIB动态代理如何选择?
答题要点(建议按优先级排序回答):
优先JDK动态代理:若目标类实现了接口,优先使用JDK,无需引入第三方依赖
选择CGLIB:若目标类未实现接口,只能用CGLIB
两者都能用时:JDK 8以后性能差距不大,选JDK更轻量;需注意CGLIB无法代理
final方法
加分点:Spring AOP默认对实现了接口的类使用JDK动态代理,对没有实现接口的类自动切换到CGLIB-47。
Q4:CGLIB为什么无法代理final类或final方法?
答题要点:CGLIB通过生成目标类的子类来实现代理,子类必须能够覆盖父类方法。final类不能被继承,final方法不能被覆盖,因此无法代理-47。
Q5:动态代理的底层依赖什么技术?有什么性能影响?
答题要点:
JDK动态代理依赖反射机制,每次方法调用都有反射开销
CGLIB动态代理依赖ASM字节码操作,通过生成子类避免反射,但生成代理对象时开销较大-32
实际项目中通常采用单例模式复用代理对象,减少频繁创建的性能损耗
九、总结
本文围绕代理模式,从静态代理到动态代理,从JDK实现到CGLIB实现,梳理了以下核心知识点:
| 核心要点 | 一句话记忆 |
|---|---|
| 代理模式价值 | 不修改原有代码,实现功能增强,业务与增强解耦 |
| 静态代理 vs 动态代理 | 静态“一对一”手写,动态“一对多”运行时生成 |
| JDK动态代理 | 基于接口+反射,需实现接口,无需额外依赖 |
| CGLIB动态代理 | 基于继承+字节码,无接口限制,不能代理final类 |
| 动态代理底层 | JDK依赖反射,CGLIB依赖ASM字节码生成 |
| 面试避坑 | 分清“思想 vs 实现”、“接口 vs 继承” |
下一期预告:从代理模式到Spring AOP源码,带你追踪动态代理在Spring框架中的落地实现。敬请期待!
