Java的代理模式

代理模式

代理模式

在 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 动态代理的核心设计逻辑:

  1. 调用方发起请求(租客提租房需求):客户端执行id.dosomething(),即租客向中介门店(动态代理对象 id)提出租房请求,触发整个代理流程的启动;
  2. 代理对象转发请求(中介门店传需求):动态代理对象 id 自身不承载任何业务或增强逻辑,仅作为请求转发入口,将租房请求直接转发至绑定的代理处理器 dp(中介店长);
  3. 处理器触发核心逻辑(店长启动服务流程):代理对象的请求转发行为触发处理器 dp 的invoke方法执行,标志着店长开始按标准化流程处理租房需求;
  4. 执行前置增强逻辑(店长收中介费):invoke方法优先执行前置增强代码(System.out.println("前置工作")),对应租房场景中店长在核心租房动作前收取中介费的操作;
  5. 反射调用核心业务(店长对接房东完成租房):通过method.invoke(obj, arg)反射调用真实目标对象 idi 的dosomething()方法,即店长联系房东(InterDemoImpl),由房东完成 "租房" 核心动作(打印 "doing");
  6. 执行后置增强逻辑(店长扣押金):核心业务执行完成后,invoke方法继续执行后置增强代码(System.out.println("后置工作")),对应租房场景中店长在租房完成后扣除押金的操作;
  7. 返回执行结果(店长告知租客结果):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,仅适合资深开发者操作。
相关推荐
Tony Bai3 小时前
高并发后端:坚守 Go,还是拥抱 Rust?
开发语言·后端·golang·rust
wjs20244 小时前
Swift 类型转换
开发语言
没有bug.的程序员4 小时前
服务安全:内部服务如何防止“裸奔”?
java·网络安全·云原生安全·服务安全·零信任架构·微服务安全·内部鉴权
一线大码4 小时前
SpringBoot 3 和 4 的版本新特性和升级要点
java·spring boot·后端
秃了也弱了。4 小时前
python实现定时任务:schedule库、APScheduler库
开发语言·python
weixin_440730504 小时前
java数组整理笔记
java·开发语言·笔记
weixin_425023004 小时前
Spring Boot 实用核心技巧汇总:日期格式化、线程管控、MCP服务、AOP进阶等
java·spring boot·后端
一线大码4 小时前
Java 8-25 各个版本新特性总结
java·后端
Thera7774 小时前
状态机(State Machine)详解:原理、优缺点与 C++ 实战示例
开发语言·c++
2501_906150565 小时前
私有部署问卷系统操作实战记录-DWSurvey
java·运维·服务器·spring·开源