设计模式-代理模式

从概念到前后端实战

1. 为什么需要代理?(问题导向)

在工程实践中,我们常常需要在不修改原有业务代码 的前提下,为对象透明地 加上缓存、鉴权、限流、重试、日志、懒加载等横切能力。直接在业务类里加,会让代码变得臃肿、难维护、难测试------这就是 Proxy 代理模式 大显身手的地方。

一句话 :代理模式为某个对象提供一个"替身(Proxy)",由代理在调用前/后做额外处理,再把请求转发给真实对象(RealSubject)。


2. 定义与角色

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

  • 核心角色

    • Subject:抽象主题(接口/抽象类)。
    • RealSubject:真实主题(被代理者)。
    • Proxy:代理对象,持有 RealSubject 的引用,负责访问控制/增强

3. 常见分类与应用举例

  • 远程代理(Remote Proxy):本地类代理远端服务调用(RPC、微服务)。
  • 虚拟代理(Virtual Proxy):按需/懒加载大对象(例如图片懒加载、复杂图表)。
  • 保护代理(Protection Proxy):基于权限角色控制访问(RBAC)。
  • 缓存/智能引用代理(Smart Reference):透明加缓存、引用计数、日志、性能埋点。
  • 防护代理:限流、熔断、降级、重试、超时控制。

这些都能在"不动核心业务"的情况下,由代理进行横切增强。


4. 何时使用?(Checklist)

  • 需要在不修改原类的前提下,新增通用能力(缓存/鉴权/日志/限流)。
  • 真实对象创建或调用开销大 ,希望懒加载按需
  • 访问需要前置校验(登录、角色、白名单)。
  • 需要统一调用链打点调用重试熔断/超时

5. 和相近模式怎么区分?

模式 关注点 是否改变接口 典型用途
代理(Proxy) 控制访问 & 透明增强 不变(对 Client 透明) 鉴权、缓存、远程、懒加载
装饰器(Decorator) 叠加新职责 不变 功能扩展(如为组件"加特效")
适配器(Adapter) 接口不兼容的转换 改变为适配接口 旧接口适配新系统
外观(Facade) 提供统一入口 通常不暴露原接口 简化复杂子系统调用

6. 静态代理(TypeScript 入门示例)

适用于接口稳定、代理逻辑较少的场景。优点是类型清晰;缺点是类多、扩展性一般。

ts 复制代码
// Subject
export interface UserService {
  getProfile(userId: string): Promise<{ id: string; name: string }>;
}

// RealSubject
export class RemoteUserService implements UserService {
  async getProfile(userId: string) {
    // 模拟远程调用
    await new Promise(r => setTimeout(r, 50));
    return { id: userId, name: "Alice" };
  }
}

// Proxy:增加鉴权与日志
export class UserServiceProxy implements UserService {
  constructor(private real: UserService, private currentRole: "guest" | "admin") {}

  async getProfile(userId: string) {
    const start = performance.now();
    if (this.currentRole === "guest") {
      throw new Error("Permission denied");
    }
    const result = await this.real.getProfile(userId);
    const cost = Math.round(performance.now() - start);
    console.info(`[UserService.getProfile] cost=${cost}ms`);
    return result;
  }
}

// 使用
const service = new UserServiceProxy(new RemoteUserService(), "admin");
service.getProfile("1001").then(console.log);

7. 动态代理(JavaScript/TypeScript 的 ES6 Proxy

JS 原生 Proxy 能动态拦截 任意对象 的属性读取、设置、函数调用,是前端/Node 最常用的"代理模式"载体。

7.1 通用 AOP 代理工厂

ts 复制代码
type AnyFn = (...args: any[]) => any;

interface Hooks {
  before?: (prop: string, args: unknown[]) => void;
  after?: (prop: string, ret: unknown) => void;
  error?: (prop: string, e: unknown) => void;
}

export function createAopProxy<T extends object>(target: T, hooks: Hooks): T {
  return new Proxy(target, {
    get(t, p, r) {
      const prop = String(p);
      const val = Reflect.get(t, p, r);
      if (typeof val !== "function") return val;
      return new Proxy(val as AnyFn, {
        apply(fn, thisArg, args) {
          try {
            hooks.before?.(prop, args);
            const ret = Reflect.apply(fn, t, args);
            if (ret instanceof Promise) {
              return ret
                .then(x => { hooks.after?.(prop, x); return x; })
                .catch(e => { hooks.error?.(prop, e); throw e; });
            }
            hooks.after?.(prop, ret);
            return ret;
          } catch (e) {
            hooks.error?.(prop, e);
            throw e;
          }
        }
      });
    }
  });
}

7.2 组合能力:缓存 + 限流 + 重试 + 超时

ts 复制代码
// 简易 TTL 缓存
class TTLCache<K, V> {
  private m = new Map<K, { v: V; exp: number }>();
  constructor(private ttl = 5_000) {}
  get(k: K) {
    const x = this.m.get(k);
    if (!x) return undefined;
    if (Date.now() > x.exp) { this.m.delete(k); return undefined; }
    return x.v;
  }
  set(k: K, v: V) { this.m.set(k, { v, exp: Date.now() + this.ttl }); }
}

// 简易令牌桶限流
class TokenBucket {
  private tokens: number; private last = Date.now();
  constructor(private ratePerSec: number, private capacity = ratePerSec) { this.tokens = capacity; }
  allow() {
    const now = Date.now();
    const refill = ((now - this.last) / 1000) * this.ratePerSec;
    this.tokens = Math.min(this.capacity, this.tokens + refill);
    this.last = now;
    if (this.tokens >= 1) { this.tokens -= 1; return true; }
    return false;
  }
}

// 工具:带超时与重试
async function withTimeout<T>(p: Promise<T>, ms: number) {
  return Promise.race([
    p,
    new Promise<T>((_, rej) => setTimeout(() => rej(new Error(`Timeout ${ms}ms`)), ms))
  ]);
}

async function withRetry<T>(fn: () => Promise<T>, times = 2, backoff = 200): Promise<T> {
  let last: unknown;
  for (let i = 0; i <= times; i++) {
    try { return await fn(); } catch (e) { last = e; await new Promise(r => setTimeout(r, backoff * Math.pow(2, i))); }
  }
  throw last;
}

// 为"远程服务"生成防护代理
function protectRemote<T extends object>(svc: T, opt: { ttl?: number; rps?: number; timeout?: number; retry?: number }) {
  const cache = new TTLCache<string, unknown>(opt.ttl ?? 3000);
  const bucket = new TokenBucket(opt.rps ?? 5);
  return createAopProxy(svc, {
    before(prop, args) {
      const key = `${prop}:${JSON.stringify(args)}`;
      const cached = cache.get(key);
      if (cached !== undefined) throw { __cached: true, key, cached };
      if (!bucket.allow()) throw new Error("Rate limit exceeded");
    },
    async after(prop, ret) {
      if (ret instanceof Promise) return; // 上面已在 AOP 工厂处理
      const key = `${prop}:` + "??"; // 简化演示
      cache.set(key, ret);
    },
    error(prop, e) {
      if ((e as any).__cached) return; // 通过异常短路返回缓存(也可改用线程局部变量)
    }
  });
}

说明:真实生产会把"缓存命中"改为正常返回,这里用抛错短路是为了演示 Hook 流程。你也可以在 get 里直接返回包装函数。


8. 前端实战

8.1 虚拟代理:图片懒加载(IntersectionObserver)

ts 复制代码
// HTML: <img class="lazy" data-src="https://...big.jpg" />
const io = new IntersectionObserver(entries => {
  entries.forEach(e => {
    if (e.isIntersecting) {
      const img = e.target as HTMLImageElement;
      img.src = img.dataset.src || "";
      io.unobserve(img);
    }
  });
});

document.querySelectorAll("img.lazy").forEach(img => io.observe(img));

8.2 保护代理:防止重复提交

ts 复制代码
function once<T extends (...a: any[]) => Promise<any>>(fn: T): T {
  let pending = false;
  return (async function (...args: any[]) {
    if (pending) return; pending = true;
    try { return await fn(...args); } finally { pending = false; }
  }) as T;
}

const submit = once(async () => {
  // 调用真实提交
  await fetch("/api/submit", { method: "POST" });
});

8.3 请求代理:自动附带 Token + 刷新

ts 复制代码
interface Http {
  get<T>(url: string, init?: RequestInit): Promise<T>;
}

class FetchImpl implements Http {
  async get<T>(url: string, init?: RequestInit) {
    const res = await fetch(url, init);
    if (!res.ok) throw new Error(res.statusText);
    return res.json();
  }
}

function authProxy(http: Http, getToken: () => Promise<string>, refresh: () => Promise<void>): Http {
  return new Proxy(http, {
    get(t, p, r) {
      const val = Reflect.get(t, p, r);
      if (typeof val !== "function") return val;
      return async function (this: any, ...args: any[]) {
        const token = await getToken();
        try {
          return await (val as Function).apply(t, [args[0], { ...(args[1]||{}), headers: { Authorization: `Bearer ${token}` } }]);
        } catch (e: any) {
          if (e.message.includes("401")) { await refresh(); }
          throw e;
        }
      }
    }
  });
}

9. Node/后端实战:API 代理(限流 + 熔断 + 缓存)

以 Express 为例,在 BFF(Backend for Frontend)层做代理:

ts 复制代码
import express from "express";
import fetch from "node-fetch";

const app = express();
let circuitOpen = false; let failCount = 0;
const cache = new Map<string, { data: any; exp: number }>();

app.get("/api/weather", async (req, res) => {
  const key = "weather:tw:taipei";
  const now = Date.now();
  const hit = cache.get(key);
  if (hit && hit.exp > now) return res.json({ cached: true, ...hit.data });

  if (circuitOpen) return res.status(503).json({ message: "circuit open" });
  try {
    const r = await Promise.race([
      fetch("https://third.party/weather?city=taipei"),
      new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), 1500))
    ]);
    // @ts-ignore
    const data = await r.json();
    cache.set(key, { data, exp: now + 10_000 });
    failCount = 0;
    res.json(data);
  } catch (e) {
    if (++failCount >= 3) { circuitOpen = true; setTimeout(() => { circuitOpen = false; failCount = 0; }, 10_000); }
    res.status(502).json({ message: "upstream error", detail: String(e) });
  }
});

app.listen(3000);

10. Java 动态代理(JDK Proxy)

在 Java 里,java.lang.reflect.Proxy + InvocationHandler 可在运行时为接口生成代理类(AOP 的基础之一)。

java 复制代码
public interface OrderService { String place(String sku, int num); }

public class OrderServiceImpl implements OrderService {
  @Override public String place(String sku, int num) { return "ok:" + sku + ":" + num; }
}

import java.lang.reflect.*;

public class LogProxy implements InvocationHandler {
  private final Object target;
  public LogProxy(Object target) { this.target = target; }

  @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    long s = System.currentTimeMillis();
    try {
      Object ret = method.invoke(target, args);
      System.out.println(method.getName() + " ok cost=" + (System.currentTimeMillis()-s));
      return ret;
    } catch (InvocationTargetException e) {
      System.out.println(method.getName() + " error: " + e.getTargetException().getMessage());
      throw e.getTargetException();
    }
  }

  @SuppressWarnings("unchecked")
  public static <T> T of(T target) {
    return (T) Proxy.newProxyInstance(
      target.getClass().getClassLoader(),
      target.getClass().getInterfaces(),
      new LogProxy(target)
    );
  }
}

// 使用
OrderService s = LogProxy.of(new OrderServiceImpl());
System.out.println(s.place("SKU-1", 2));

提示:JDK 动态代理只能代理接口 ;如需代理类可用 CGLIB(基于继承/字节码增强)。


11. 在 Umi/React 项目中的落地建议

  • 请求层 :给 request/fetch 封一个 代理工厂(统一加 token、重试、超时、熔断、缓存)。
  • 组件层 :对高代价可视化(大图表、地图)用虚拟代理 懒加载;交互按钮用保护代理防重复提交。
  • BFF 层 :在 Node/Express/Nest 架一层 反向代理 ,实现限流/缓存/熔断,保护后端微服务。
  • 日志与监控:用 AOP 代理统一埋点(method 入参/耗时/异常)。

12. 常见坑与最佳实践

  • this 绑定 :JS Proxy 里转发时优先 Reflect.applyFunction.prototype.apply.call,保证 this 与原对象一致。
  • 性能 :代理引入额外开销------对高频/极短执行方法谨慎使用;将部分逻辑下沉到框架层(中间件/拦截器)。
  • 错误处理 :异步链路要统一处理异常,避免"吞错"。
  • 可观测性 :给代理增加日志 TraceId指标,否则问题难排查。
  • 序列化Proxy 对象不能直接被结构化克隆(如 postMessage),需要转原始对象或 DTO。

13. 测试建议

  • Proxy 行为写单测

    • 鉴权失败是否拦截;
    • 缓存命中是否减少真实调用;
    • 限流/熔断是否生效;
    • 日志/埋点是否被调用(可用 spy/mock)。

14. 小结(带走这张速查表)

  • 要点:代理 = 透明访问控制 + 横切增强;不改原接口,专注"调用前/后"的能力。

  • 常用组合拳:鉴权 + 缓存 + 限流 + 重试 + 超时 + 熔断 + 日志(AOP)。

  • 落地路径

    1. 先抽象 Subject 接口;
    2. 用静态或动态代理封装横切能力;
    3. 在请求层/组件层/BFF 层按需插拔;
    4. 加上可观测性与单测,闭环迭代。

如果你正在做性能与稳定性治理,优先把"代理化"到请求与服务边界 ------ 成本最低,收益最大。


15. 延伸阅读(关键词)

  • JS 原生 ProxyReflect API;
  • Java Proxy / InvocationHandler、CGLIB;
  • AOP(面向切面编程)、拦截器(Interceptor)、中间件(Middleware);
  • 断路器(Circuit Breaker)、令牌桶/漏桶限流、幂等;
  • 虚拟代理(懒加载)、保护代理(RBAC)。

相关推荐
mask哥4 小时前
DP-观察者模式代码详解
java·观察者模式·微服务·设计模式·springboot·设计原则
柯南二号7 小时前
【Android】【设计模式】抽象工厂模式改造弹窗组件必知必会
android·设计模式·抽象工厂模式
一支鱼15 小时前
leetcode-4-寻找两个正序数组的中位数
算法·leetcode·typescript
Pure031915 小时前
Spring MVC BOOT 中体现的设计模式
spring·设计模式·mvc
ytadpole19 小时前
揭秘设计模式:优雅地为复杂对象结构增添新功能-访问者模式
java·设计模式
烛阴20 小时前
TypeScript 进阶必修课:解锁强大的内置工具类型(二)
前端·javascript·typescript
我的征途是星辰大海。1 天前
java-设计模式-5-创建型模式-建造
java·设计模式
蒋星熠1 天前
Vue 3 + TypeScript 现代前端开发最佳实践(2025版指南)
前端·vue.js·人工智能·pytorch·深度学习·ai·typescript
叫我阿柒啊1 天前
从Java全栈到前端框架:一场真实面试的深度技术探索
java·redis·微服务·typescript·vue3·springboot·jwt