代理模式:静态代理、动态代理与AOP实现原理

代理模式,说白了就是「找人替你办事」。你想想看,生活中我们经常需要代理——买房找中介、打官司找律师、上网用VPN。在软件世界里,代理模式干的也是同样的事:一个对象不想直接暴露自己,或者想在干活前后加点私货,就找个代理替它出面。

我做了十几年架构,代理模式几乎每天都在用。尤其是做微服务和AOP的时候,没有代理模式,很多功能根本没法优雅实现。今天我就把静态代理、动态代理(JDK和CGLIB)、AOP原理,以及C++里的代理实现,一次性给你讲透。

1. 代理模式的核心思想

先看一张图,把代理模式的整体结构刻在脑子里:

代理模式结构图 Client(客户端) Subject(抽象接口) RealSubject(真实对象) Proxy(代理对象) 代理对象和真实对象实现同一接口,客户端只跟代理打交道

核心就三句话:

  • 抽象接口:定义业务方法
  • 真实对象:真正干活的类
  • 代理对象:替真实对象接活,可以在前后加逻辑

客户端只跟代理交互,不知道背后是谁在干活。这就是「控制访问」的精髓。

2. 静态代理——最朴素的实现

静态代理,就是在编译期就把代理类写好。每个真实对象对应一个代理类,一对一。

我记得刚入行时,项目经理让我给所有Service方法加日志。我傻乎乎地每个类都写一个代理……那叫一个酸爽。后来才知道,这种硬编码的方式就是静态代理。

Java 静态代理示例

// 1. 抽象接口
public interface UserService {
    void addUser(String name);
    void deleteUser(int id);
}

// 2. 真实对象
public class UserServiceImpl implements UserService {
    @Override
    public void addUser(String name) {
        System.out.println("添加用户:" + name);
    }
    @Override
    public void deleteUser(int id) {
        System.out.println("删除用户:" + id);
    }
}

// 3. 代理对象
public class UserServiceProxy implements UserService {
    private UserService target;
    
    public UserServiceProxy(UserService target) {
        this.target = target;
    }
    
    @Override
    public void addUser(String name) {
        System.out.println("[日志] 开始添加用户");
        target.addUser(name);
        System.out.println("[日志] 添加用户结束");
    }
    
    @Override
    public void deleteUser(int id) {
        System.out.println("[日志] 开始删除用户");
        target.deleteUser(id);
        System.out.println("[日志] 删除用户结束");
    }
}

静态代理的痛点:每个方法都要手动写代理逻辑,接口一改,代理类也得改。10个接口就要写10个代理类,维护成本爆炸。

3. 动态代理——JDK Proxy

静态代理太笨重,于是Java在1.3版本引入了动态代理。你只需要写一个InvocationHandler,就能给任意接口生成代理对象。

说白了,动态代理就是在运行时「凭空捏造」一个代理类出来。我当年第一次看到Proxy.newProxyInstance时,觉得这简直是黑魔法。

JDK动态代理示例

import java.lang.reflect.*;

public class LogHandler implements InvocationHandler {
    private Object target;
    
    public LogHandler(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("[日志] 方法调用结束");
        return result;
    }
}

// 使用方式
UserService target = new UserServiceImpl();
UserService proxy = (UserService) Proxy.newProxyInstance(
    target.getClass().getClassLoader(),
    target.getClass().getInterfaces(),
    new LogHandler(target)
);
proxy.addUser("张三");

注意:JDK动态代理只能代理接口,不能代理类。如果目标对象没有实现接口,JDK Proxy就无能为力了。我曾经在这个坑里摔过一次——接手一个老项目,全是具体类,没有接口,想加日志都加不了。

4. CGLIB动态代理——代理类也能玩

JDK Proxy只能代理接口,那没有接口的类怎么办?CGLIB就是来填这个坑的。

CGLIB的原理是:通过字节码技术,生成目标类的子类作为代理。说白了,它不是在接口层面做文章,而是直接继承目标类,重写它的方法。

CGLIB动态代理示例

import net.sf.cglib.proxy.*;

public class CglibProxy implements MethodInterceptor {
    private Object target;
    
    public Object getProxy(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(target.getClass());
        enhancer.setCallback(this);
        return enhancer.create();
    }
    
    @Override
    public Object intercept(Object obj, Method method, Object[] args,
            MethodProxy proxy) throws Throwable {
        System.out.println("[CGLIB] 前置处理");
        Object result = proxy.invoke(target, args);
        System.out.println("[CGLIB] 后置处理");
        return result;
    }
}

// 使用方式
UserServiceImpl target = new UserServiceImpl();
UserServiceImpl proxy = (UserServiceImpl) 
    new CglibProxy().getProxy(target);
proxy.addUser("李四");

CGLIB的局限:不能代理final类,也不能代理final方法。因为它是通过继承实现的,final类不能被继承,final方法不能被重写。另外,CGLIB在生成代理类时性能开销比JDK Proxy大,但运行时性能差不多。

5. JDK Proxy vs CGLIB 对比

对比维度 JDK Proxy CGLIB
原理 基于接口,生成实现类 基于继承,生成子类
目标要求 必须实现接口 不能是final类
生成速度 快(反射生成) 慢(字节码操作)
运行速度 较慢(反射调用) 较快(直接调用)
适用场景 面向接口编程 无接口的类

6. AOP实现原理——代理模式的高级应用

AOP(面向切面编程),说白了就是「在不修改源码的情况下,给方法加逻辑」。日志、事务、权限校验……这些横切关注点,用AOP处理最合适。

Spring AOP的底层就是代理模式:

  • 如果目标对象实现了接口 → 用JDK Proxy
  • 如果目标对象没有实现接口 → 用CGLIB

我参与过一个支付系统的重构,原来每个支付方法里都手动写日志和事务,代码重复率高达60%。后来用AOP一把梭,核心业务代码减少了40%,维护起来舒服多了。

AOP核心概念

// 切面:日志切面
@Aspect
@Component
public class LogAspect {
    
    // 切入点:匹配所有Service方法
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void servicePointcut() {}
    
    // 前置通知
    @Before("servicePointcut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("调用方法:" + joinPoint.getSignature().getName());
    }
    
    // 后置通知
    @After("servicePointcut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("方法结束:" + joinPoint.getSignature().getName());
    }
    
    // 环绕通知
    @Around("servicePointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = pjp.proceed();
        long elapsed = System.currentTimeMillis() - start;
        System.out.println("耗时:" + elapsed + "ms");
        return result;
    }
}

AOP的本质:Spring在启动时,扫描到@Aspect注解,解析切入点表达式,然后为匹配的Bean生成代理对象。代理对象在调用目标方法时,会先执行切面逻辑,再调用真实方法。整个过程对业务代码完全透明。

7. C++中的代理模式实现

C++没有Java那样的反射机制,也没有动态代理。但C++有模板、虚函数和智能指针,照样能实现代理模式。

我个人习惯在C++中用两种方式实现代理:

  • 基于虚函数的静态代理:和Java静态代理类似,手动写代理类
  • 基于模板的通用代理:用模板和函数包装器实现类似动态代理的效果

C++静态代理示例

#include <iostream>
#include <memory>

// 抽象接口
class Image {
public:
    virtual void display() = 0;
    virtual ~Image() = default;
};

// 真实对象
class RealImage : public Image {
private:
    std::string filename;
public:
    RealImage(const std::string& f) : filename(f) {
        loadFromDisk();
    }
    void display() override {
        std::cout << "显示图片: " << filename << std::endl;
    }
private:
    void loadFromDisk() {
        std::cout << "从磁盘加载: " << filename << std::endl;
    }
};

// 代理对象
class ProxyImage : public Image {
private:
    std::unique_ptr<RealImage> realImage;
    std::string filename;
public:
    ProxyImage(const std::string& f) : filename(f) {}
    void display() override {
        // 懒加载:第一次使用时才创建真实对象
        if (!realImage) {
            realImage = std::make_unique<RealImage>(filename);
        }
        std::cout << "[代理] 记录日志" << std::endl;
        realImage->display();
    }
};

int main() {
    ProxyImage proxy("photo.jpg");
    // 第一次调用,触发加载
    proxy.display();
    // 第二次调用,直接使用缓存
    proxy.display();
    return 0;
}

C++模板通用代理

#include <iostream>
#include <functional>
#include <memory>

// 通用代理模板
template<typename T>
class Proxy {
private:
    std::shared_ptr<T> target;
    std::function<void()> beforeHook;
    std::function<void()> afterHook;
public:
    Proxy(std::shared_ptr<T> t) : target(t) {}
    
    void setBeforeHook(std::function<void()> hook) {
        beforeHook = hook;
    }
    void setAfterHook(std::function<void()> hook) {
        afterHook = hook;
    }
    
    // 通过重载->操作符实现方法拦截
    T* operator->() {
        if (beforeHook) beforeHook();
        // 这里无法直接拦截方法调用,需要配合其他机制
        return target.get();
    }
};

// 使用示例
class Service {
public:
    void doWork() {
        std::cout << "执行核心业务" << std::endl;
    }
};

int main() {
    auto service = std::make_shared<Service>();
    Proxy<Service> proxy(service);
    proxy.setBeforeHook([](){
        std::cout << "[日志] 方法开始" << std::endl;
    });
    proxy->doWork();
    return 0;
}

C++代理的局限:C++没有Java那样的动态代理机制,无法在运行时动态生成代理类。但通过模板和函数对象,我们仍然可以实现灵活的代理逻辑。如果追求更强大的AOP效果,可以考虑使用AspectC++这样的AOP扩展。

8. 避坑指南

我曾经踩过几个代理模式的坑,分享给你:

  • JDK Proxy强制转换异常:如果目标对象没有实现接口,用JDK Proxy会抛出ClassCastException。解决办法是改用CGLIB,或者在设计时就坚持面向接口编程。
  • CGLIB代理final方法失效:CGLIB无法重写final方法,如果目标类有final方法,代理不会生效。我建议在需要代理的类中,避免使用final关键字。
  • 循环代理问题:AOP中如果切面方法内部又调用了同一个类的其他方法,可能会触发无限递归。解决办法是用AopContext.currentProxy()获取当前代理对象。
  • 性能陷阱:动态代理在频繁创建和销毁的场景下性能较差。如果代理对象需要频繁创建,可以考虑使用对象池或者静态代理。

9. 总结

代理模式是设计模式中应用最广泛的一个,没有之一。从简单的日志记录,到复杂的AOP框架,再到RPC远程调用,处处都有代理的身影。

我个人建议:

  • 如果项目是面向接口编程,优先用JDK Proxy
  • 如果遇到没有接口的遗留代码,用CGLIB
  • 如果做大型项目,直接用Spring AOP,它帮你封装好了这一切
  • C++项目中,根据需求选择静态代理或模板代理

记住一句话:代理模式的核心不是「代理」本身,而是「控制」。控制访问、控制行为、控制生命周期——这才是代理模式的灵魂。


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