代理模式:为其他对象提供一种代理以控制对这个对象的访问

代理模式,说白了就是「找个中间人」。你想想看,生活中我们经常这么干——找中介租房、找律师打官司、找秘书接电话。程序里的代理模式,道理一模一样。

我刚开始接触这个模式时,觉得它有点多余。直接调用对象不就行了?干嘛非要绕一圈?后来在项目中踩过坑才明白,有些场景下,你确实需要一个人替你「挡在前面」。

为什么需要代理?

直接访问对象,有时候会出问题。比如:

  • 对象创建很耗时,你不想每次都用的时候才创建
  • 对象在远程服务器上,你不能直接 new 一个
  • 你想在访问前后加点额外逻辑,比如权限检查、日志记录

这些场景下,代理模式就派上用场了。代理对象和被代理对象实现同一个接口,客户端只跟代理打交道。代理在背后,帮你处理那些「脏活累活」。

核心思想:代理对象控制着对真实对象的访问。客户端不知道也不关心背后是谁在干活。

三种代理实现方式

Java 世界里,代理有三种主流实现。我按自己的理解,把它们分成三个层次:

代理类型 原理 适用场景 个人评价
静态代理 手动编写代理类 代理逻辑固定,类少 简单粗暴,但代码冗余
JDK 动态代理 运行时生成代理对象 被代理对象有接口 灵活,但必须面向接口
CGLIB 代理 通过继承生成子类 被代理对象没有接口 强大,但注意 final 方法

静态代理:最原始的方式

静态代理就是你自己写一个代理类,实现和目标类相同的接口。每个方法里,你手动调用目标方法,并在前后加上自己的逻辑。

我在早期的一个项目中用过这种方式。当时要给一个支付接口加日志,我直接写了个 PaymentProxy,里面包了一个 PaymentServiceImpl。代码倒是清晰,但后来接口多了,代理类也跟着膨胀。嗯,维护起来挺痛苦的。

// 接口
public interface PaymentService {
    void pay(String account, double amount);
}

// 真实对象
public class PaymentServiceImpl implements PaymentService {
    public void pay(String account, double amount) {
        System.out.println("支付 " + amount + " 到 " + account);
    }
}

// 静态代理
public class PaymentServiceProxy implements PaymentService {
    private PaymentService target;

    public PaymentServiceProxy(PaymentService target) {
        this.target = target;
    }

    public void pay(String account, double amount) {
        System.out.println("[日志] 开始支付");
        target.pay(account, amount);
        System.out.println("[日志] 支付完成");
    }
}

我的建议:静态代理适合代理类很少、逻辑固定的场景。如果代理逻辑经常变,或者代理类超过三五个,赶紧换动态代理。

JDK 动态代理:运行时生成

JDK 动态代理利用 java.lang.reflect.ProxyInvocationHandler,在运行时动态创建代理对象。你只需要写一个 InvocationHandler,所有方法调用都会经过它。

我记得第一次用动态代理时,感觉挺神奇的。不用写那么多代理类了,一个 InvocationHandler 搞定所有。但有个坑——被代理对象必须实现接口。如果你的类没有接口,JDK 动态代理就无能为力了。

public class LogHandler implements InvocationHandler {
    private Object target;

    public LogHandler(Object target) {
        this.target = target;
    }

    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("[日志] 方法结束");
        return result;
    }
}

// 使用
PaymentService proxy = (PaymentService) Proxy.newProxyInstance(
    PaymentService.class.getClassLoader(),
    new Class[]{PaymentService.class},
    new LogHandler(new PaymentServiceImpl())
);
proxy.pay("张三", 100.0);

注意:JDK 动态代理只能代理接口。如果你的类没有实现任何接口,运行时会抛出 IllegalArgumentException。我曾经在这个坑里爬了半天才出来。

CGLIB 代理:没有接口也能代理

CGLIB 通过继承目标类来生成子类,子类就是代理对象。它不要求目标类实现接口,所以比 JDK 动态代理更通用。但有个限制——final 方法和 final 类无法被代理,因为 CGLIB 靠继承,而 final 方法不能被重写。

Spring 框架里,如果 Bean 实现了接口,默认用 JDK 动态代理;如果没有接口,就用 CGLIB。这个设计挺聪明的,兼顾了灵活性和性能。

// 目标类,没有接口
public class UserService {
    public void saveUser(String name) {
        System.out.println("保存用户: " + name);
    }
}

// CGLIB 代理
public class CglibProxy implements MethodInterceptor {
    private Enhancer enhancer = new Enhancer();

    public Object getProxy(Class clazz) {
        enhancer.setSuperclass(clazz);
        enhancer.setCallback(this);
        return enhancer.create();
    }

    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println("[CGLIB] 方法前");
        Object result = proxy.invokeSuper(obj, args);
        System.out.println("[CGLIB] 方法后");
        return result;
    }
}

// 使用
UserService proxy = (UserService) new CglibProxy().getProxy(UserService.class);
proxy.saveUser("李四");

个人经验:能用 JDK 动态代理就用 JDK,性能更好。CGLIB 是备选方案,别一上来就用它。另外,CGLIB 对 final 方法无能为力,设计类时要注意。

三种代理的对比

我整理了一张对比表,方便你快速决策:

维度 静态代理 JDK 动态代理 CGLIB 代理
是否需要接口
代理类数量 每个目标类一个 一个 InvocationHandler 一个 MethodInterceptor
性能 高(编译期) 中(反射调用) 中(字节码增强)
灵活性
限制 代码冗余 必须实现接口 不能代理 final 类/方法

代理模式的核心逻辑

下面这张图,展示了代理模式的核心结构。你看一眼就能明白:客户端只跟代理打交道,代理在背后控制着对真实对象的访问。

客户端 调用 代理对象 - 权限检查 - 日志记录 转发 真实对象 - 业务逻辑 - 数据操作 客户端只与代理对象交互,代理对象控制对真实对象的访问 代理层

避坑指南

代理模式用起来顺手,但坑也不少。我把自己踩过的坑列出来,你注意避开:

  • 不要滥用代理——每个方法都加代理,性能会下降。只对需要控制访问的方法加代理。
  • 注意代理链的顺序——多个代理嵌套时,顺序很重要。比如先做权限检查,再做日志记录,顺序反了可能出问题。
  • CGLIB 代理的 final 陷阱——我曾经给一个 Service 类加了 final 方法,结果 CGLIB 代理后调用那个方法,直接走了父类逻辑,没走代理逻辑。排查了半天才发现。
  • JDK 动态代理的转型问题——代理对象只能转型为接口类型,不能转型为具体实现类。否则会抛 ClassCastException。

曾经踩过的坑:有一次我用 JDK 动态代理代理了一个没有接口的类,运行时报错。后来换成 CGLIB 才解决。所以,设计阶段就要想清楚:你的类有没有接口?如果没有,提前准备好 CGLIB 依赖。

总结

代理模式不是什么高深的技术,它就是「找个中间人」这个朴素思想的体现。三种实现方式各有优劣,选哪个取决于你的具体场景:

  • 代理类少、逻辑固定 → 静态代理
  • 有接口、需要灵活 → JDK 动态代理
  • 没有接口、需要代理 → CGLIB 代理

我个人更倾向于 JDK 动态代理,因为它干净、轻量。但遇到没有接口的类,CGLIB 就是救命稻草。记住一点:代理模式是为了控制访问,不是为了炫技。别为了用代理而用代理,想清楚你要解决什么问题。


公众号:蓝海资料掘金营,微信deep3321