代理模式详解 — 静态代理与动态代理

一个类实现了和目标类相同的接口,持有目标对象,方法调用时委托给目标,并可在前后加额外逻辑

代理模式 静态代理 动态代理 InvocationHandler Proxy.newProxyInstance AOP 思想

目录导航(点击跳转)

一、代理模式的核心思想

1 一句话定义
代理模式:一个类(代理)实现了和目标类相同的接口,内部持有目标对象的引用,方法调用时委托给目标对象执行真正的逻辑,并可以在委托前后添加额外逻辑
2 三个核心要素
要素含义为什么需要
① 实现相同接口 代理类和目标类都 implement 同一个 Interface 调用方不关心是代理还是目标——对调用方透明
② 持有目标引用 代理内部保存一个目标对象的引用(构造器传入或运行时指定) 代理自己不干活,最终要委托给真正的干活人
③ 委托 + 增强 代理方法里:先做额外的事 → 调 target.xxx()→ 再做额外的事 不修改目标代码的前提下增加功能
3 伪代码本质

如果把代理模式的本质用伪代码表达,就是:

// 代理类的"灵魂"就在这一个方法里
public void doWork() {
    // ===== 前置增强:代理的附加逻辑 =====
    before();

    // ===== 委托:真正干活的是目标对象 =====
    target.doWork();

    // ===== 后置增强:代理的附加逻辑 =====
    after();
}

静态代理和动态代理的区别,仅仅在于这个代理类是怎么来的

  • 静态代理:你手写 StaticProxy.java,编译时就有了
  • 动态代理:JVM 在运行时动态生成代理类的字节码,没 .java文件
4 生活中的类比:明星与经纪人
品牌方(调用方)
1联系经纪人谈合作
谈合作
结果
经纪人 = 代理
2筛选邀约(前置逻辑)
3问明星要不要接(委托)
4签合同排档期(后置逻辑)
明星 = 目标(真正干活)

品牌方不知道也不需要知道明星在干嘛(代理对调用方透明),但最终还是要明星亲自去拍广告、唱歌、演戏(真正的活还得目标对象干)。经纪人(代理)帮明星过滤邀约、谈价格、排档期——不修改明星的行为,但增加了额外功能

二、静态代理 — 手动写代理类

1 完整代码演示
// ========== 1. 定义接口 ==========
interface Service {
    void doWork();
}

// ========== 2. 目标类(真正干活的) ==========
class RealService implements Service {
    public void doWork() {
        System.out.println("RealService: 干活");
    }
}

// ========== 3. 代理类(增强 + 委托) ==========
class StaticProxy implements Service {
    private Service target;          // 持有目标对象的引用

    public StaticProxy(Service target) {
        this.target = target;        // 通过构造器注入目标
    }

    public void doWork() {
        System.out.println("前置:日志");   // 前置增强
        target.doWork();                   // 委托给目标
        System.out.println("后置:事务");   // 后置增强
    }
}

// ========== 4. 使用 ==========
public class Main {
    public static void main(String[] args) {
        Service real = new RealService();        // 创建目标对象
        Service proxy = new StaticProxy(real);   // 用代理包装目标
        proxy.doWork();                          // 调用代理,代理增强 + 委托
    }
}

输出:

前置:日志
RealService: 干活
后置:事务
2 静态代理的执行流程
调用方
proxy.doWork()
StaticProxy
1. 打印"前置:日志"
RealService
2. target.doWork()
StaticProxy
3. 打印"后置:事务"
返回
调用结束
3 特点与优缺点
特点说明
代理类在编译前就存在StaticProxy.java是实实在在的 .java文件,编译成 .class,运行时直接用
一对一绑定接口一个 StaticProxy 只能代理实现了 Service接口的类,换一个接口就要新写一个代理类

优点

  • 简单直接:逻辑一目了然,容易理解
  • 编译期检查:类型错误编译时就能发现
  • 无反射开销:直接方法调用,性能最优

缺点

  • 代码量大:每需要一个代理就要手写一个类
  • 不易维护:接口变了所有代理类都要改
  • 重复代码:多个代理类的前后逻辑往往是重复的

三、动态代理 — 运行时生成代理类

1 核心 API 解析

Java 动态代理的核心是 java.lang.reflect.ProxyInvocationHandler

API作用
Proxy.newProxyInstance(...) 运行时动态生成代理类的字节码,并返回代理实例
InvocationHandler.invoke(...) 代理实例的每个方法调用都会转发到这个 invoke 方法中

newProxyInstance()需要三个参数:

  1. ClassLoader:用哪个类加载器加载生成的代理类(通常用目标类的 ClassLoader)
  2. 接口数组:代理类要实现哪些接口(可同时代理多个接口)
  3. InvocationHandler:所有方法调用都转发到这个 handler 的 invoke 方法
2 完整代码演示
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// ========== 接口和目标类同上 ==========
interface Service {
    void doWork();
}

class RealService implements Service {
    public void doWork() {
        System.out.println("RealService: 干活");
    }
}

// ========== 使用动态代理 ==========
public class DynamicProxyDemo {
    public static void main(String[] args) {
        // 1. 创建目标对象
        Service real = new RealService();

        // 2. 创建动态代理实例
        Service proxy = (Service) Proxy.newProxyInstance(
            real.getClass().getClassLoader(),      // 参数1:类加载器
            real.getClass().getInterfaces(),        // 参数2:目标实现的接口
            new InvocationHandler() {               // 参数3:调用处理器
                @Override
                public Object invoke(
                        Object proxy,               // 代理实例自身
                        Method method,              // 被调用的方法
                        Object[] args               // 方法参数
                ) throws Throwable {
                    System.out.println("前置:日志");            // 前置增强
                    // 反射调用目标对象的真正方法
                    Object result = method.invoke(real, args); // 委托
                    System.out.println("后置:事务");            // 后置增强
                    return result;
                }
            }
        );

        // 3. 调用代理方法
        proxy.doWork();
    }
}

输出与静态代理完全一致:

前置:日志
RealService: 干活
后置:事务
3 动态代理的执行流程
1
proxy.doWork()被调用
2
JVM 拦截方法调用,转发到 InvocationHandler.invoke()
3
invoke 方法执行:前置增强 → method.invoke(target, args)→ 后置增强
4
返回结果给调用方
关键理解:动态代理生成的代理类没有 .java 源文件,它是 JVM 在运行时直接生成字节码的。一个 InvocationHandler可以同时代理多个接口,所有接口的所有方法调用都走同一个 invoke()方法。
4 invoke 方法的三个参数详解
参数类型含义何时用
proxy Object 代理实例自身(即 Proxy.newProxyInstance返回的那个对象) 一般不直接用,只用来获取代理类信息
method java.lang.reflect.Method 当前被调用的方法(如 Service.doWork 用来反射调用目标对象:method.invoke(target, args)
args Object[] 方法调用时传入的参数 透传给目标方法
常见坑:不要在 invoke 里调用 proxy参数的方法(如 proxy.toString()),这会导致无限递归——因为调用代理方法又会触发 invoke,invoke 里又调代理方法,死循环直到栈溢出。
5 Proxy 对象的作用 — 为什么需要代理对象?

一个自然的疑问:既然代理对象的每个方法都只是转发给 handler.invoke(),那为什么不直接调用 handler.invoke()呢?为什么还需要 Proxy.newProxyInstance()生成的那个代理对象?

答案是:Proxy 对象不是可有可无的中间人,它承担着不可替代的关键角色。

作用说明
类型适配器 把通用的 InvocationHandler包装成具体的业务接口(如 CalculatorGithubService),让调用方以自然、类型安全的方式使用
方法转发器 代理对象上的每个接口方法,实现体都只有一行:handler.invoke(...)。它自己不干活,只是把调用转发给背后的 InvocationHandler
隐藏反射细节 调用者无需手动获取 Method对象、包装参数、处理反射异常,只需像调用普通对象一样写 proxy.add(3, 5)
保证接口契约 代理对象实现了你指定的接口,因此可以通过编译器的类型检查,也能在 IDE 中获得自动补全
多接口支持 一个代理对象可以同时实现多个接口,而 InvocationHandler只是一个普通对象,不具备多接口类型

用代码对比来感受差异:

// =====  没有 Proxy:调用方必须手动处理反射 =====
InvocationHandler handler = new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("前置:日志");
        Object result = method.invoke(real, args);
        System.out.println("后置:事务");
        return result;
    }
};

// 调用方每次都要手动反射,痛苦!
Method method = Calculator.class.getMethod("add", int.class, int.class);
Object result = handler.invoke(null, method, new Object[]{3, 5});
// 没有类型安全,编译器不检查,IDE 不补全,异常要 try-catch


// =====  有 Proxy:调用方像调用普通对象一样自然 =====
Calculator proxy = (Calculator) Proxy.newProxyInstance(
    real.getClass().getClassLoader(),
    real.getClass().getInterfaces(),
    handler
);

int result = proxy.add(3, 5);  // 类型安全!IDE 自动补全!编译器检查!
// 调用者完全不知道背后有 InvocationHandler 在干活
一句话概括:Proxy 对象是动态代理的"外壳",它让 InvocationHandler这个通用处理器能够以特定接口的形态被调用,从而兼顾了灵活性与类型安全

InvocationHandler是"策略",代理对象是"外壳"——两者配合:外壳保证接口契约,策略提供具体实现。

四、静态代理 vs 动态代理 对比

1 核心区别对比表
维度静态代理动态代理(JDK)
代理类何时生成 编译前(开发者手写 .java) 运行时(JVM 动态生成字节码)
是否有 .java 文件 没有
代理类数量 N 个接口 → N 个代理类 1 个 InvocationHandler → N 个接口
灵活性 低,接口变了代理类也要改 高,handler 是通用的
性能 高,直接方法调用 稍低,有反射开销
(但现代 JVM 已大幅优化反射性能)
是否必须基于接口 不必须(可继承类) 必须基于接口
(JDK 动态代理的限制)
可代理的接口数 任意(手动写) 可同时代理多个接口
2 共同点:都满足代理模式的三要素
实现相同接口 持有目标引用 委托 + 增强

无论静态还是动态,代理的核心思想都是一样的:代理站在调用方和目标之间,既能让调用方无感使用(接口相同),又能拦截每个方法调用(委托 + 增强)。

3 选型指南
需要代理
有接口?
有接口 → 代理接口数量?
1-2 个接口 → 静态代理(简单直接)
|
3+ 个接口、通用增强 → 动态代理(灵活)
无接口(需代理类)→ JDK 动态代理不行,考虑 CGLIB

五、代理模式的应用场景

1 典型应用场景
场景说明例子
日志记录 方法调用前后自动打日志 log("开始调用: " + method.getName())
事务管理 方法前开事务,方法后提交/回滚 Spring 的 @Transactional就是通过代理实现的
权限校验 方法调用前检查是否有权限 if (!hasPermission) throw new SecurityException()
性能监控 统计方法执行时间 long start = System.nanoTime(); ... long cost = ...
远程调用(RPC) 代理封装网络通信细节 Retrofit 的接口代理,调用方法时实际发网络请求
延迟加载 第一次访问时才真正初始化目标对象 Hibernate 的懒加载代理
核心价值:所有这些场景的共同点是——不改目标代码,通过代理织入额外逻辑。这就是 AOP(面向切面编程)的思想基础。Spring AOP 底层就是基于动态代理实现的。

六、核心思想总结

三句话记住代理模式

  1. 代理 = 相同接口 + 持有目标 + 委托调用 + 前后增强——这是代理模式的不变量,无论静态还是动态都一样
  2. 静态代理 = 手动写代理类(编译前就写好 .java 文件),动态代理 = JVM 运行时生成(Proxy.newProxyInstance + InvocationHandler)
  3. 动态代理的精髓:一个 InvocationHandler 的 invoke 方法接管了代理实例的所有方法调用——它就像一个"万能拦截器",无论调用哪个方法都会先经过它

静态代理与动态代理的本质差异

静态代理

代理类来源:手写的 .java文件 → 编译成 .class→ 直接使用
代理粒度:一个代理类绑定一个接口
代码量:N 个接口 = N 个代理类 = 大量重复代码
适用:接口少、逻辑简单、追求极致性能

动态代理

代理类来源:JVM 运行时直接生成字节码 → 没有 .java文件
代理粒度:一个 InvocationHandler 可代理多个接口
代码量:一个 handler 通吃所有接口
适用:接口多、通用增强、AOP 场景

代理模式的好处

  • 对调用方透明:调用方不知道也不关心调用的是代理还是目标
  • 不修改目标代码:增强逻辑完全在代理中,目标类保持纯净
  • 职责分离:目标专注业务逻辑,代理专注横切关注点(日志、事务等)
  • 灵活组合:可以多层代理嵌套(代理套代理)

代理模式的限制

  • JDK 动态代理必须基于接口:如果目标类没实现任何接口,JDK 代理用不了
  • 代理类内部调用不会触发代理:目标方法 A 调 B,A 被代理了但 B 不会走代理
  • 动态代理有反射开销:虽然现代 JVM 优化很好,但理论上比直接调用慢