代理模式
代理模式
在 Java 开发中,代理模式是一种重要的结构型设计模式,其核心思想是为目标对象提供一个代理对象,由代理对象控制对目标对象的访问。代理模式的核心价值是在不修改目标对象源码的前提下,对目标对象的方法进行增强(如添加日志、权限校验、事务管理等),同时还能控制目标对象的创建时机、封装远程访问过程等。
在 Java 实际开发中,代理模式按代理类的生成时机可分为静态代理和动态代理,其中动态代理又包含 JDK 动态代理、CGLib 动态代理两种主流实现方式;Spring AOP(默认 JDK 动态代理,无接口时用 CGLib)、MyBatis Mapper 代理(基于 JDK 动态代理)等框架的核心底层均依赖动态代理技术。
本文将详细介绍静态代理、JDK 动态代理、CGLib 动态代理这三种实现方式,并对比它们的特性与适用场景。
静态代理
静态代理(Static Proxy)是最基础的代理实现方式,特点是在编译时手动编写代理类,代理类与目标类遵循面向接口编程原则,共同实现同一个抽象接口(或继承同一抽象类),代理类通过依赖抽象接口持有目标对象引用,而非直接依赖具体实现类。
静态代理要求代理类和目标类实现同一个目标接口,代理类内部持有接口类型的目标对象引用,并通过构造器接收目标对象实例;当调用代理类的方法时,会先执行前置增强逻辑,再调用目标对象的核心业务方法,最后执行后置增强逻辑,且每次调用都会完整执行该流程。
静态代理代码示例
核心接口:HireHouse
java
public interface HireHouse {
public void hire();
}
定义代理类和目标类的统一行为规范,是静态代理的核心抽象层,保证调用方可以通过接口统一访问目标对象或代理对象。
目标类(真实业务类):HireHouseReal
java
public class HireHouseReal implements HireHouse {
public void hire() {
System.out.println("租房子");
}
}
实现核心接口,仅聚焦核心业务逻辑,符合单一职责原则;即使脱离代理类,目标类也能独立运行。
代理类:HireHouseProxy
java
public class HireHouseProxy implements HireHouse {
// 持有真实业务类的对象
private HireHouseReal hr;
public void hire() {
// 懒加载逻辑,只有第一次调用hire()时,才创建真实对象
if(hr == null) {
hr = new HireHouseReal();
System.out.println("收租金和中介费和押金");
hr.hire(); // 调用真实业务类的核心方法
System.out.println("扣押金");
}
}
}
private HireHouseReal hr;:
代理类持有目标类的实例引用,这是代理类能代理目标类的关键前提。只有持有目标类引用,才能在代理类中调用目标类的核心方法,实现控制目标类访问的核心思想。if(hr == null)懒加载逻辑:
设计第一次调用 hire() 才创建目标类实例,是静态代理控制目标对象创建时机的典型体现(而非提前初始化目标对象);- 核心代理逻辑:
- 前置增强:
System.out.println("收租金和中介费和押金");在调用目标类核心方法前,代理类添加的额外功能,体现静态代理 "不修改目标类源码,增强核心业务" 的价值; - 核心业务调用:
hr.hire();代理类最终调用目标类的核心方法,完成核心动作,这是代理类的代理本质; - 后置增强:
System.out.println("扣押金");在核心业务完成后,代理类添加的额外操作,体现增强逻辑与核心业务解耦的设计思路。
- 前置增强:
这段代码模拟了 "租客→中介(代理类)→房东(目标类)" 的代理场景,契合静态代理的核心价值:
租客(调用方)不直接与房东(目标类)交互,而是通过中介(代理类)完成租房流程,体现代理类控制对目标类的访问;
中介除了帮租客完成 "租房子" 核心动作,还额外处理 "收中介费、扣押金" 等操作,且未修改房东的任何代码,体现静态代理 "不修改目标类源码,增强方法功能" 的核心优势。
静态代理的优缺点
静态代理的优点
- 执行效率高:静态代理类在编译阶段就已生成对应的 class 字节码文件,无额外运行时开销,执行效率是所有代理方式中最高的。
- 逻辑清晰、易于调试与维护:代理类的增强逻辑都是显式手动编写的,代码结构直观,开发人员能快速定位代理逻辑的执行流程。
- 符合设计原则,解耦核心业务与增强逻辑:
目标类仅聚焦核心业务逻辑,额外的非核心功能(如日志、权限)全部交由代理类实现,严格遵循单一职责原则;
同时符合 "开闭原则":无需修改目标类源码,仅通过新增 / 修改代理类即可扩展目标类功能,避免对核心业务代码的侵入。 - 编译期类型安全:代理类与目标类必须实现同一接口,编译阶段就能校验两者的方法一致性,可提前暴露类型错误,避免动态代理中运行时才出现的类型转换异常。
静态代理的缺点
- 代码冗余度高:一个接口需要编写一个对应的代理类,若系统中存在大量业务接口,则需编写大量重复的代理类代码,即使多个代理类的增强逻辑完全相同,也需在每个代理类中重复编写,代码复用性差。
- 维护成本高:一旦业务接口发生变更,所有实现该接口的目标类,以及对应的代理类都必须同步修改;接口越核心、关联的目标类 / 代理类越多,修改范围越大,维护成本呈指数级上升。
- 灵活性与扩展性差
代理类只能代理固定接口的目标对象,无法在运行时动态适配不同接口的目标类;
代理行为完全在编译期确定,无法在运行时动态调整增强逻辑,也无法动态切换代理的目标对象;
若需为不同接口的目标类添加相同的增强逻辑,只能在多个代理类中重复编写,无法复用增强逻辑。
JDK动态代理
为解决静态代理一个接口对应一个代理类的代码冗余问题,Java 提供了动态代理机制 ------JDK 动态代理。
其核心特点是运行时动态生成代理类字节码并加载到 JVM,无需手动编写代理类;在同一套增强逻辑下,一个代理处理器(InvocationHandler)可适配多个目标类(只要目标类实现接口)。
核心原理
JDK 动态代理依赖 Java 反射机制,核心是 InvocationHandler 接口(代理处理器)和 Proxy 类:
- 约束条件:目标类必须实现至少一个接口(因 JDK 动态代理生成的代理类默认继承 Proxy,Java 单继承机制决定代理类只能通过实现接口绑定目标类);
- 生成逻辑:Proxy.newProxyInstance() 方法会在运行时生成一个实现目标接口的代理类字节码,加载后实例化出代理对象;
- 调用逻辑:当调用代理对象的任意接口方法时,会触发绑定的 InvocationHandler 的 invoke 方法 ------ invoke 方法会接收目标方法、方法参数、目标对象等参数,开发者可在该方法中统一编写增强逻辑,并通过反射调用目标对象的核心方法。
JDK动态代理代码示例
被代理的核心接口:InterDemo
java
public interface InterDemo {
// 定义被代理的核心方法
public void dosomething();
}
JDK 动态代理的核心前提是目标类必须实现接口,该接口是动态生成的代理对象与真实业务类的统一行为规范。
动态生成的代理类会实现此接口,因此客户端可通过 InterDemo 类型接收代理对象,符合面向接口编程的设计思想。
真实业务类(被代理类):InterDemoImpl
java
public class InterDemoImpl implements InterDemo {
/**
* 方法实现类
*/
@Override
public void dosomething() {
System.out.println("doing");
}
}
这是实现核心接口的真实业务类,仅负责执行核心业务逻辑,符合单一职责原则。即使脱离代理逻辑,该类也能独立运行,所有增强逻辑均与核心业务解耦,不侵入真实业务代码。
代理处理器(核心):DynamicProxyImpl
java
public class DynamicProxyImpl implements InvocationHandler {
// 被代理的对象(用Object类型,适配所有类)
private Object obj;
// 构造方法:绑定真实对象
public DynamicProxyImpl(Object obj) {
super();
this.obj = obj;
}
/**
* 所有代理方法的调用都会转发到这个方法
* @param arg0 动态生成的代理对象本身(一般命名为proxy,这里用arg0)
* @param method 被调用的核心方法
* @param arg 方法调用时的参数(dosomething()无参,所以是null)
* @return 核心方法的执行结果
* @throws Throwable 方法执行异常
*/
@Override
public Object invoke(Object arg0, Method method, Object[] arg)throws Throwable {
System.out.println("前置工作"); // 前置增强逻辑
Object result = method.invoke(obj, arg); // 反射调用真实对象的核心方法
System.out.println("后置工作"); // 后置增强逻辑
return result;
}
}
private Object obj;:采用 Object 类型而非具体类型持有真实对象,是实现动态代理通用性的核心设计,使得该处理器可适配所有实现接口的目标类,无需为不同类编写专属处理器。- 构造方法:通过传入真实对象实例,将处理器与被代理的真实对象绑定,为后续通过反射调用真实对象的方法提供基础。
- invoke 方法(代理逻辑的核心入口):
- 前置增强:调用真实对象的核心方法前,执行System.out.println("前置工作"),完成日志、权限校验等前置增强逻辑;
- 反射调用核心方法:method.invoke(obj, arg) 等价于直接调用绑定到处理器的真实对象实例,这是动态代理替代静态代理硬编码调用目标方法的关键;
- 后置增强:核心方法执行完成后,执行System.out.println("后置工作"),完成事务提交、资源释放等后置增强逻辑;
客户端测试类:Client(调用入口)
java
public class Client {
public static void main(String[] args) {
// 创建真实业务对象(被代理的对象)
InterDemoImpl idi = new InterDemoImpl();
// 创建代理处理器,绑定真实对象
DynamicProxyImpl dp = new DynamicProxyImpl(idi);
// 动态生成代理对象
InterDemo id = (InterDemo) Proxy.newProxyInstance(
idi.getClass().getClassLoader(),
idi.getClass().getInterfaces(),
dp
);
// 通过代理对象调用方法(实际转发到invoke方法)
id.dosomething();
}
}
Proxy.newProxyInstance() 会在内存中动态生成一个代理类的字节码(无物理.class 文件),并创建该代理类的实例:
类加载器 idi.getClass().getClassLoader() 让动态生成的代理类和真实类在同一个类加载器下,避免类加载冲突;
接口数组 idi.getClass().getInterfaces() 生成的代理类要实现 InterDemo 接口,因此代理对象可以强转为InterDemo类型;
InvocationHandler dp 绑定代理处理器,代理对象的所有方法调用都会转发到 dp 的 invoke 方法
为更直观理解执行逻辑,将 JDK 动态代理的核心组件与租房场景做精准映射:
- 真实业务类(InterDemoImpl)→ 房东:唯一能完成 "租房" 核心动作的主体,聚焦核心业务逻辑;
- 动态生成的代理对象(id)→ 中介门店:租客直接接触的交互载体,自身不承载任何业务 / 增强逻辑,仅负责转发租客的租房请求;
- 代理处理器(DynamicProxyImpl)→ 中介店长:中介门店的核心决策层,承接门店转发的所有请求,统一处理 "额外操作(增强逻辑)与对接房东(调用目标类方法)" 全流程。
JDK 动态代理执行流程:
该场景中,"租客(Client 类 main 方法,调用方)→ 中介(动态生成的代理对象 id + 代理处理器 DynamicProxyImpl)→ 房东(目标类 InterDemoImpl)" 的交互链路,契合 JDK 动态代理的核心设计逻辑:
- 调用方发起请求(租客提租房需求):客户端执行id.dosomething(),即租客向中介门店(动态代理对象 id)提出租房请求,触发整个代理流程的启动;
- 代理对象转发请求(中介门店传需求):动态代理对象 id 自身不承载任何业务或增强逻辑,仅作为请求转发入口,将租房请求直接转发至绑定的代理处理器 dp(中介店长);
- 处理器触发核心逻辑(店长启动服务流程):代理对象的请求转发行为触发处理器 dp 的invoke方法执行,标志着店长开始按标准化流程处理租房需求;
- 执行前置增强逻辑(店长收中介费):invoke方法优先执行前置增强代码(System.out.println("前置工作")),对应租房场景中店长在核心租房动作前收取中介费的操作;
- 反射调用核心业务(店长对接房东完成租房):通过method.invoke(obj, arg)反射调用真实目标对象 idi 的dosomething()方法,即店长联系房东(InterDemoImpl),由房东完成 "租房" 核心动作(打印 "doing");
- 执行后置增强逻辑(店长扣押金):核心业务执行完成后,invoke方法继续执行后置增强代码(System.out.println("后置工作")),对应租房场景中店长在租房完成后扣除押金的操作;
- 返回执行结果(店长告知租客结果):invoke方法将核心业务的执行结果返回给调用方,即店长向租客反馈租房流程已完成(本示例中dosomething()为 void 类型,返回值为 null,无实际业务意义,但符合 JDK 动态代理的结果返回规范)。
JDK动态代理的优缺点
JDK动态代理的优点
- 无侵入式增强,符合开闭原则
JDK 动态代理的增强逻辑完全封装在InvocationHandler接口实现类中,仅通过接口绑定实现功能扩展。这一特性严格遵循 "对扩展开放、对修改关闭" 的开闭原则,保障目标类的核心业务逻辑纯粹性,避免增强逻辑对核心代码的侵入。 - 高代码复用性,解决静态代理冗余问题
单个InvocationHandler实现类(代理处理器)可适配所有实现接口的目标类,只需编写一次增强逻辑(如日志、权限校验),即可作用于不同业务模块的目标类。相比静态代理 "一个接口对应一个代理类" 的冗余设计,大幅减少重复代码,提升代码复用率与可维护性。 - 动态性强,降低开发与维护成本
代理类由 JVM 在运行时动态生成字节码并加载,无需开发者手动编写代理类文件。当业务接口发生变更(如新增方法)时,仅需调整InvocationHandler的invoke方法逻辑,无需修改代理类(因无物理代理类),维护成本远低于静态代理;同时可在运行时动态切换目标对象或增强逻辑,灵活性显著优于静态代理。 - 编译期类型安全,降低运行时风险
JDK 动态代理要求目标类必须实现接口,代理类与目标类共同遵循接口的方法约束。编译阶段可直接校验方法匹配性,提前暴露类型错误;而运行时代理对象可直接强转为接口类型,避免无接口代理场景下的类型转换异常,提升代码可靠性。 - 轻量级原生实现,集成成本低
基于 JDK 内置的 java.lang.reflect 包(Proxy类、InvocationHandler接口)实现,无需依赖第三方库,与 Java 生态天然兼容,项目集成无额外依赖引入成本,且底层实现稳定、可追溯。
JDK动态代理的缺点
- 强依赖接口,适用范围受限
目标类必须实现至少一个接口是 JDK 动态代理的硬性约束,对于无接口的纯业务类(如工具类、未规范设计的业务类),JDK 动态代理无法生效,存在明显的使用场景限制。 - 反射机制引入性能损耗
代理对象的方法调用最终会通过Method.invoke()反射调用目标类方法,相比静态代理的直接方法调用,反射涉及方法签名解析、参数封装 / 解封装、权限校验等额外操作,存在一定性能开销。尽管 JVM 对反射有即时编译优化,普通场景下损耗可忽略,但在高频调用(如高并发接口)场景中,性能差异会被放大。 - 代理范围有限,无法覆盖类方法
JDK 动态代理仅能代理接口中定义的公共方法,目标类中未实现接口的方法(如 private、protected 修饰的方法,或接口外的 public 方法)无法被代理;即使方法在接口中定义,若目标类中实现为final(不可重写),反射调用时会抛出IllegalAccessException,同样无法代理。 - 增强逻辑集中化,易导致代码臃肿
所有代理对象的方法调用都会转发至同一个InvocationHandler的invoke方法,若需为不同目标方法配置差异化增强逻辑,需在invoke方法中通过Method对象做大量条件判断,易导致invoke方法逻辑臃肿、可读性下降,增加代码维护难度。 - 无法精细控制代理类生成过程
JDK 动态代理封装了代理类的生成逻辑,开发者无法自定义代理类的类名、访问修饰符、父类之外的接口等细节;若需对代理类做定制化修改(如添加自定义注解、实现额外接口),JDK 动态代理无原生支持,需依赖字节码修改工具,灵活性弱于底层字节码操作的动态代理方案(如 CGLib)。
CGLib动态代理
CGLib(Code Generation Library)是一款基于 ASM 字节码操作框架实现的动态代理工具,其核心价值在于弥补了 JDK 动态代理的核心局限性 ------JDK 动态代理仅能代理实现接口的类,无法对无接口的普通业务类进行代理,而 CGLib 突破了这一接口约束。
CGLib 的核心特征是:无需目标类实现任何接口,而是通过运行时动态生成目标类的子类字节码并加载,将该子类作为代理类;代理类会重写目标类中所有非 final 的方法(final 方法无法被重写,因此无法代理),并将增强逻辑植入重写后的方法中。当调用代理对象的方法时,会触发预设的拦截器逻辑,先执行前置 / 后置增强操作,再调用目标类的原方法,最终实现对无接口类的代理增强。
核心组件:
Enhancer:CGLib 的核心类,用于配置代理类的父类(目标类)、设置方法拦截器,最终生成代理对象;
MethodInterceptor:方法拦截器(类比 JDK 的InvocationHandler),增强逻辑写在intercept方法中,所有代理对象的方法调用都会转发到该方法。
CGLib动态代理代码示例
需先引入 CGLib 依赖
xml
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>3.3.0</version>
</dependency>
目标类(无需实现接口)
java
public class DemoService {
// 核心业务方法(不能是final)
public void dosomething() {
System.out.println("doing");
}
}
CGLib 方法拦截器(核心增强逻辑)
java
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
// 方法拦截器
public class CglibProxyInterceptor implements MethodInterceptor {
/**
* 所有代理对象的方法调用都会转发到该方法
* @param proxy 动态生成的代理对象(目标类的子类实例)
* @param method 被调用的目标方法
* @param args 方法参数(本示例无参,为null)
* @param methodProxy 方法代理对象(用于快速调用目标方法,性能优于反射)
* @return 目标方法的执行结果
* @throws Throwable 方法执行异常
*/
@Override
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
// 前置增强逻辑
System.out.println("前置工作");
// 调用目标类的核心方法
Object result = methodProxy.invokeSuper(proxy, args);
// 后置增强逻辑
System.out.println("后置工作");
return result;
}
}
客户端测试类(调用入口)
java
import net.sf.cglib.proxy.Enhancer;
public class Client {
public static void main(String[] args) {
// 创建增强器(CGLib核心类,用于生成代理对象)
Enhancer enhancer = new Enhancer();
// 设置代理类的父类(指定目标类)
enhancer.setSuperclass(DemoService.class);
// 设置方法拦截器(绑定增强逻辑)
enhancer.setCallback(new CglibProxyInterceptor());
// 生成代理对象(运行时动态生成DemoService的子类实例)
DemoService proxy = (DemoService) enhancer.create();
// 调用代理对象的方法(触发intercept方法)
proxy.dosomething();
}
}
代码执行流程(类比租房场景)
- 初始化增强器:Enhancer类比 "中介总公司",负责配置代理规则(指定父类 = 房东,绑定拦截器 = 店长);
- 生成代理对象:enhancer.create()运行时动态生成DemoService的子类(代理类),并创建实例,该代理对象类比 "中介门店",是房东(DemoService)的 "子类";
- 调用代理方法:proxy.dosomething()(租客找中介租房),代理对象的dosomething()是重写后的方法,直接转发到CglibProxyInterceptor的intercept方法;
- 执行增强逻辑:
前置增强:打印 "前置工作"(店长收中介费);
调用目标方法:methodProxy.invokeSuper(proxy, args)(店长对接房东,执行核心业务 "打印 doing");
后置增强:打印 "后置工作"(店长扣押金); - 返回结果:将目标方法执行结果返回给调用方(告知租客租房完成)。
CGLib动态代理的优缺点
CGLib动态代理的优点
- 突破接口约束,适用范围更广
对于无接口的普通业务类、工具类,或因历史设计未规范实现接口的遗留类,CGLib 均可完成代理增强,无需为了适配代理而强行让目标类实现无业务意义的接口,适配场景覆盖度高于 JDK 动态代理。 - 执行性能更优(相对 JDK 动态代理)
CGLib 通过MethodProxy.invokeSuper直接调用代理子类重写的方法,绕开了 JDK 动态代理中Method.invoke()的反射机制(反射涉及方法签名解析、参数封装 / 权限校验等额外开销)。 - 增强逻辑的灵活性更高
基于 ASM 字节码层面的操作特性,CGLib 可对目标类的方法做更精细的拦截控制:除了普通成员方法,还可拦截目标类的构造方法、静态方法,甚至能在字节码层面修改目标方法的执行逻辑;相比之下,JDK 动态代理仅能拦截接口定义的方法,灵活度远低于 CGLib。 - 保持目标类设计的纯粹性
无需为了实现代理而侵入目标类的设计,符合最小侵入原则。目标类可完全聚焦自身核心业务逻辑,无需兼顾代理的适配要求,降低了代码设计的耦合度。
CGLib动态代理的缺点
- 继承机制带来的硬性约束
CGLib 通过生成目标类的子类实现代理,受 Java 单继承机制限制:
目标类若被final修饰,不可继承则无法生成代理子类,直接导致代理失败;目标类中的final方法无法被子类重写,因此这类方法无法植入增强逻辑,无法被代理。 - 依赖第三方库,增加集成成本
CGLib 并非 JDK 原生功能,需引入第三方依赖(如cglib核心包),且其底层依赖 ASM 字节码框架,易出现 ASM 版本冲突;而 JDK 动态代理基于 java.lang.reflect 原生包,无额外依赖成本。 - 代理类初始化成本更高
CGLib 在运行时需动态生成目标类的子类字节码,并完成类加载、实例化,这一过程的开销高于 JDK 动态代理(JDK 仅生成实现接口的代理类,字节码结构更简单)。尽管该开销仅在首次创建代理对象时产生(后续可复用),但在对启动速度敏感的场景中,可能带来轻微的延迟。 - 字节码操作的开发门槛高
若需自定义 CGLib 的代理类生成逻辑(如修改字节码、添加自定义注解、扩展代理类功能),需基于 ASM 框架进行字节码层面的开发,学习成本和开发难度远高于 JDK 动态代理的反射 API,仅适合资深开发者操作。