代理模式完全指南:从静态代理到 Spring AOP 的工程化巅峰

代理模式可能是最被"实际使用得最多,但意识到自己在用的次数最少"的设计模式 —— 因为它太基础了。Spring AOP、Hibernate 懒加载、RPC 框架、CDN、虚拟代理、安全代理,这些工程基础设施全是代理。这篇文章把代理从原理讲到 JDK 动态代理、CGLib、Mockito,讲清楚它和装饰器、适配器、外观的边界,以及"动态代理"这个让代理威力倍增的关键技术。

代理要解决什么

代理本质是在客户端和真实对象之间放一个"中间人"。这个中间人和真实对象长得一样(实现相同接口),客户端用它和用真实对象在调用方式上没有区别。但中间人内部可以做很多事:

  • 权限控制:校验是否能访问。
  • 延迟加载:真实对象太重,真正用到时才创建。
  • 缓存:结果可缓存的就不再调真实对象。
  • 远程调用:真实对象在远端,代理负责网络传输。
  • 计数 / 日志 / 监控:统计调用次数、记录调用日志。
  • 事务管理:开启事务、提交、回滚。

代理的标准结构

// 1. 共同接口
interface ImageService {
    Image load(String url);
}

// 2. 真实主体
class RealImageService implements ImageService {
    public Image load(String url) {
        System.out.println("从网络加载 " + url + "(慢)");
        return downloadAndDecode(url);
    }
}

// 3. 代理:和真实主体实现同一接口,内部持有真实主体的引用
class CachingImageProxy implements ImageService {
    private final ImageService real = new RealImageService();
    private final Map<String, Image> cache = new HashMap<>();

    public Image load(String url) {
        if (cache.containsKey(url)) {
            System.out.println("从缓存命中 " + url);
            return cache.get(url);
        }
        Image img = real.load(url);
        cache.put(url, img);
        return img;
    }
}

// 客户端:依赖接口,不关心拿到的是真实对象还是代理
ImageService svc = new CachingImageProxy();
svc.load("a.png");   // 从网络加载
svc.load("a.png");   // 缓存命中

关键认知:代理和真实对象对调用方表现完全相同。客户端只看到 ImageService 接口,代理或真实对象可以无感替换。

代理的几种类型

1. 虚拟代理(Virtual Proxy):延迟加载

class LazyImageProxy implements ImageService {
    private Image realImage;     // 还没创建
    private final String url;

    public LazyImageProxy(String url) { this.url = url; }

    public Image load(String url) {
        if (realImage == null) realImage = downloadAndDecode(url);   // 真用到才加载
        return realImage;
    }
}

典型例子:Hibernate 的懒加载、Word 文档里的"图片占位符"(滚动到时才真正加载)。

2. 保护代理(Protection Proxy):权限控制

class AdminImageProxy implements ImageService {
    private final ImageService real;
    private final User currentUser;

    public Image load(String url) {
        if (!currentUser.hasRole("admin")) throw new AccessDeniedException();
        return real.load(url);
    }
}

这是 Spring Security 的 @PreAuthorize 注解背后的机制 —— 生成一个代理,在调用真实方法前先校验权限。

3. 远程代理(Remote Proxy):RPC 客户端

// RPC 客户端代理:本地调用看着像普通方法,内部走网络
class UserServiceRpcProxy implements UserService {
    private final RpcClient client;
    public User getUser(long id) {
        return client.call("UserService.getUser", id);   // 序列化、网络、反序列化
    }
}

Dubbo、gRPC、Thrift、Spring Cloud Feign 都是远程代理 —— 业务代码调用 userService.getUser(1) 感觉像调本地方法,实际是远程调用。

4. 缓存代理

上面例子里的 CachingImageProxy 就是。Spring 的 @Cacheable 注解通过代理实现。

5. 日志代理 / 监控代理

class LoggingProxy implements UserService {
    private final UserService real;
    public User getUser(long id) {
        long start = System.currentTimeMillis();
        try {
            User u = real.getUser(id);
            log.info("getUser({}) returned {}, took {}ms", id, u, System.currentTimeMillis() - start);
            return u;
        } catch (Exception e) {
            log.error("getUser({}) failed", id, e);
            throw e;
        }
    }
}

静态代理的痛点

上面的代理都叫"静态代理":代理类在编译期就写好。问题:接口里有 N 个方法,代理类要实现 N 个方法 —— 每个都是"前置 + 委托 + 后置"的模板。如果有 50 个 Service,每个都要写一个 LoggingProxy,代码量爆炸。

解决方案:动态代理 —— 运行时根据接口动态生成代理类。

JDK 动态代理

JDK 自带的动态代理 —— 利用反射在运行时生成实现给定接口的代理类:

import java.lang.reflect.*;

interface UserService {
    User getUser(long id);
    void updateUser(User u);
}

class RealUserService implements UserService {
    public User getUser(long id) { return new User(id, "mores"); }
    public void updateUser(User u) { /* ... */ }
}

// InvocationHandler:统一处理代理收到的所有调用
class LoggingHandler implements InvocationHandler {
    private final Object target;
    public LoggingHandler(Object target) { this.target = target; }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            Object result = method.invoke(target, args);
            log.info("{}({}) -> {} ({}ms)",
                method.getName(), Arrays.toString(args), result, System.currentTimeMillis() - start);
            return result;
        } catch (InvocationTargetException e) {
            throw e.getTargetException();
        }
    }
}

// 创建动态代理
UserService real = new RealUserService();
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[] { UserService.class },
    new LoggingHandler(real)
);

proxy.getUser(1);
proxy.updateUser(new User(...));

关键认知:同一个 LoggingHandler 可以代理任何接口。所有方法的日志都统一在 invoke 里实现,不再需要为每个接口写代理类。

CGLib:对类(不是接口)做代理

JDK 动态代理有个限制:只能代理接口,不能代理普通类(因为它生成的代理类继承 Proxy 实现你的接口)。CGLib 通过字节码生成可以为普通类生成子类形式的代理:

import net.sf.cglib.proxy.*;

class UserService {     // 不是接口,是普通类
    public User getUser(long id) { return new User(id, "mores"); }
}

// 用 MethodInterceptor 拦截
class LoggingInterceptor implements MethodInterceptor {
    public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        log.info("call {}", method.getName());
        return proxy.invokeSuper(obj, args);   // 调用父类(真实方法)
    }
}

Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new LoggingInterceptor());
UserService proxy = (UserService) enhancer.create();

proxy.getUser(1);

Spring 默认情况下,如果被代理对象实现了接口用 JDK 动态代理,否则用 CGLib(可通过 @EnableAspectJAutoProxy(proxyTargetClass=true) 强制 CGLib)。

Spring AOP:代理模式的工程化巅峰

// 业务代码完全干净
@Service
public class OrderService {
    @Transactional             // 事务代理
    @Cacheable("orders")       // 缓存代理
    @PreAuthorize("hasRole('USER')")   // 权限代理
    public Order getOrder(long id) {
        return orderRepo.findById(id);
    }
}

// Spring 在容器启动时,根据注解给 OrderService 生成代理:
// 调用 getOrder() 时,代理 -> 鉴权 -> 看缓存 -> 开事务 -> 真实 getOrder -> 提交事务 -> 存缓存 -> 返回
@Around("@annotation(Cacheable)")
public Object cacheAround(ProceedingJoinPoint pjp) throws Throwable {
    String key = generateKey(pjp);
    Object cached = cache.get(key);
    if (cached != null) return cached;
    Object result = pjp.proceed();      // 调用真实方法
    cache.put(key, result);
    return result;
}

这套机制让横切关注点(事务、缓存、权限、日志)和业务逻辑彻底解耦。业务代码看着只有 orderRepo.findById 一行,但实际跑起来背后有一长串代理在工作。这就是 Spring 框架"轻量但功能强大"的核心秘密。

Mockito:测试里的代理

// 用 Mockito 生成一个 UserService 的代理
UserService mock = Mockito.mock(UserService.class);
when(mock.getUser(1)).thenReturn(new User(1, "test"));

// 在测试里把这个代理注入业务对象
OrderService svc = new OrderService(mock);

// 验证调用
svc.placeOrder(...);
verify(mock).getUser(eq(1));

Mockito 内部用 CGLib / ByteBuddy 给你的类生成代理,所有方法都被拦截,可以预设返回值或验证调用。这是单元测试的基础设施,每个 Java 工程师都用过。

代理 vs 装饰器 vs 适配器

三者都是"在客户端和目标之间套一层",意图不同:

  • 代理:控制访问。代理决定要不要让调用真的发生(权限、缓存、延迟)。
  • 装饰器:增强行为。被装饰对象的调用一定会发生,装饰器只是前后加事(日志、计时)。装饰器倾向于叠加多层
  • 适配器:转换接口。两边接口不同,适配器做翻译。

区分一句话:代理是"卫兵",装饰器是"装备",适配器是"翻译"

各语言里的代理

JavaScript:Proxy 是语言关键字

// JS 内置 Proxy 对象,直接拦截任何属性访问
const target = { name: 'mores', age: 30 };
const handler = {
    get(obj, prop) {
        console.log(`get ${prop}`);
        return obj[prop];
    },
    set(obj, prop, value) {
        console.log(`set ${prop} = ${value}`);
        obj[prop] = value;
        return true;
    },
};
const p = new Proxy(target, handler);

p.name;        // 打印 'get name',返回 'mores'
p.age = 31;    // 打印 'set age = 31'

// Vue 3 响应式就是用 Proxy 实现的

Python:__getattr__ 实现动态代理

class LoggingProxy:
    def __init__(self, target):
        self._target = target

    def __getattr__(self, name):
        attr = getattr(self._target, name)
        if callable(attr):
            def wrapper(*args, **kwargs):
                print(f"call {name}({args}, {kwargs})")
                return attr(*args, **kwargs)
            return wrapper
        return attr

real = UserService()
proxy = LoggingProxy(real)
proxy.get_user(1)   # 打印 call get_user((1,), {})

代理的常见坑

坑 1:Spring 中的"自调用"问题

@Service
public class OrderService {
    @Transactional
    public void outer() {
        inner();      // 直接调 inner 不走代理!事务不生效
    }

    @Transactional
    public void inner() { ... }
}

原因:Spring 代理拦截的是"从外部进入的调用"。outer() 里调 inner() 是同一个对象内部调用,绕过了代理。修复:把 inner 抽到另一个 Service,或者注入自身 proxy:

@Service
public class OrderService {
    @Autowired
    private OrderService self;        // 注入自己的代理

    public void outer() {
        self.inner();                  // 通过 self 调用,走代理
    }
}

坑 2:final 方法不能被 CGLib 代理

CGLib 靠"生成子类 + 重写方法"实现拦截,final 方法不能重写 —— 这些方法的调用绕过代理直达原方法。解决:不要在被代理类里用 final 方法,或者改用接口 + JDK 动态代理。

坑 3:动态代理性能

反射调用比直接调用慢几倍到几十倍。在每秒上万次的热路径上,代理可能成为瓶颈。优化:用 ByteBuddy 生成"非反射"代理类(直接生成字节码调原方法),Spring 5.3+ 内置类似优化。

坑 4:代理 + 序列化的坑

代理对象可能不可序列化(Proxy 类没有 readObject)。如果你不小心把一个 Spring bean 通过远程接口返回,反序列化会失败。规则:代理 stay 在服务端,跨边界传输的是 DTO 不是 Service。

识别代理场景

关键问题:

  1. 调用"真实对象"前/后要做事吗?(权限、日志、缓存)
  2. 真实对象的创建很昂贵,想推迟到第一次用?(虚拟代理)
  3. 真实对象在远端,需要本地透明调用?(远程代理)
  4. 这些"做事"逻辑要与业务代码解耦?

有 yes 就考虑代理。如果"做事"还要叠加多种(权限 + 缓存 + 日志),那是代理 + 装饰器组合,Spring AOP 就是这么干的。

写在最后

代理模式是当代 Java / Spring / .NET 生态的脊梁。注解驱动开发(@Transactional / @Cacheable / @Async)能存在,本质就是因为运行时代理 + 反射能在你的方法前后插入逻辑而不需要你手写。理解代理,你就理解了为什么一个 @Transactional 能让方法自动跑在事务里 —— 那不是魔法,那是代理。

给一个工程心法:当一段逻辑反复出现在多个方法的开头或结尾(日志、计时、权限、事务、缓存),它就是横切关注点,该用代理 / AOP 抽出去。业务代码里不该堆"开事务-业务-提交事务"这种模板,把模板交给代理,业务代码会瞬间变干净。这是设计模式里 ROI 最稳定的几个之一,几乎所有大型 Java 项目都重度依赖它。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

享元模式完全指南:从 Integer 缓存到游戏粒子系统的内存优化

2026-5-15 11:57:56

技术教程

责任链模式完全指南:从 if/else 地狱到中间件管道的工程演化

2026-5-15 15:29:19

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索