惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
NISL@THU
NISL@THU
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
GbyAI
GbyAI
SecWiki News
SecWiki News
Microsoft Azure Blog
Microsoft Azure Blog
J
Java Code Geeks
B
Blog RSS Feed
Blog — PlanetScale
Blog — PlanetScale
Schneier on Security
Schneier on Security
V
Vulnerabilities – Threatpost
C
CXSECURITY Database RSS Feed - CXSecurity.com
V
Visual Studio Blog
宝玉的分享
宝玉的分享
Recent Announcements
Recent Announcements
T
True Tiger Recordings
F
Full Disclosure
Martin Fowler
Martin Fowler
D
Docker
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
A
About on SuperTechFans
雷峰网
雷峰网
Know Your Adversary
Know Your Adversary
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
B
Blog
V
V2EX - 技术
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
S
Security Archives - TechRepublic
Google DeepMind News
Google DeepMind News
人人都是产品经理
人人都是产品经理
Malwarebytes
Malwarebytes
C
Check Point Blog
美团技术团队
P
Privacy International News Feed
Recorded Future
Recorded Future
博客园 - 司徒正美
T
The Blog of Author Tim Ferriss
L
LangChain Blog
Project Zero
Project Zero
P
Proofpoint News Feed
有赞技术团队
有赞技术团队
P
Proofpoint News Feed
Scott Helme
Scott Helme
C
CERT Recently Published Vulnerability Notes
云风的 BLOG
云风的 BLOG
T
ThreatConnect
F
Fox-IT International blog

博客园_首页

《GIS基础原理与技术实践》配套案例(Python版) - charlee44 上位机程序集的反编译与加壳保护 基于 Blazor 实现的电梯运行监测系统 - known 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (3)--- 总体思考 用 ESP32 做了一个 AI Agent 桌面状态核心,科技感直接拉满 马能否走遍棋盘的可达性证明 PortSwigger SQL注入LAB10 从 Harness Engineering 到 Trellis:AI 编程助手的工程化落地实践 信息化运维项目费用测算全指南:政策边界、三大方法与实操要点 Claude Code 实战 400 万 Tokens:接入 DeepSeek V4,从$26降到$2 Docker--容器常用命令 Dify — Chatflow - 数据库 零基础认识大语言模型工作原理 不繁花 写页面时别再把 Element Plus 整个搬进来啦!Vue3按需加载的坑我帮你踩平了 一条命令让你这辈子彻底解决"LF will be replaced by CRLF"(建议收藏) Miller Rabin:概率之下,证据成群 - Ofnoname Nessus 2026.5.9 更新升级:企业级漏扫工具的全能进阶与实战应用 Agent Harness 架构真相:Prompt Cache 如何决定 Skill、MCP 与 SubAgent 设计 Claude Code 支持 LSP 指南(C#/JAVA等) [翻译] 为什么我要用 C# 构建数据库引擎 DeepSeek V4 + Claude Code thinking mode 400 错误修复方案 云原生 CI/CD 平台架构设计 模板方法模式实战:重构Agent工具审批,告别重复代码 Ubuntu修改主机名操作指南 [MAF的Agent管道详解-03]连接LLM的IChatClient对象 《HelloGitHub》第 122 期 AI Agent 到底是做什么的?优势在哪里? 完整学习LLM(六):上下文窗口是什么,为什么模型会忘东西 和AI一起搞事情#6. 如何实现AI生图文字可编辑? 洛谷-P11105 [ROI 2023] 解密 题解 入门:我的第一个Vibe Coding实践程序 【Agentic RL / 强化学习 / OPD】OpenClaw-RL 源码阅读笔记 --- (2)--- On-Policy Distillation OpenHuman、OpenClaw、Hermes Agent 傻傻分不清楚?一篇说清三者定位 一个前端股票行情 SDK 的开源进化:从周刊收录到 v1.10.0 Claude Code 装了一堆 Skill,用了三个月,我删掉了 80% Claude Code Skill的介绍与使用 AI 漫剧账号运营教程 Hadoop(CDH6、CDP7)在Qwen3.7大模型训练中的作用,(含部署、运行操作步骤) Dify — 创建聊天机器人 -- 知识库 未来十年的数据工程:从 Modern Data Stack 到 Data Engineering Harness Java 泛型解析太痛苦?你可能需要一枚「蛋」 RAG系列:#5 RAG中的11种分块策略 看完《低智商犯罪》,学习Cypher构建知识图谱 临时邮箱的实现原理 记录一下我的 Gradle 开发环境配置过程 使用容器提供postgresql RESTful API服务 在Vue/Nuxt、React/Next/TanstackStart、RazorPages折腾一圈后,还是回到了Blazor,但这回有SSR+HTMX+Alpine的加持 把坏运气关在门外:哈希的随机化之路 agent工作模式之ReAct实战 元数据驱动开发 - 面向对象编程思想的补充 计算机科学/数据科学/人工智能/安全笔记 2026.3 前端包管理咋选?我从npm叛逃到pnpm的血泪史(附避坑指南) 深入 .NET AI Agent 开发:利用 Microsoft.Agents.AI 提取思考、调用工具与执行脚本 vibe coding(二)Where you go:一个微型 windows 桌面覆盖工具 [MAF的Agent管道详解-02]IChatClient管道如何完美连接大模型? [送码] 用 AI Coding 做了一个 App,谈谈 AI Coding 的真实体验 Claude Code 9 大神级 Skills,开发效率直接翻倍(安装、使用场景、踩坑经验) Claude Code 如何压缩上下文:Microcompact、Prompt Cache 与 cache_edits 工程拆解 Docker--Docker引擎与镜像相关命令 AScript定制left/right join查询语法 - rockey627 【学习笔记】《Python编程 从入门到实践》第3章:Python列表完全指南——创建、修改、删除与排序 - lunzi_fly PolyMarket Ghost Fills(幽灵订单)探究 - ACai_sec 面试官:说一下 Agent 的常见范式,如何选型? - 一枫说码 基于ONNXRuntime C#实现的高性能YOLO推理框架 基于 SkiaSharp 的 WPF & AvaloniaUI 极简动图播放方案 贩卖焦虑的时代,我终于接住了真实的焦虑 西安交大最新综述!一文带你读懂大模型智能体及其组网与安全 【Application Insights】采样率对Function App日志收集的影响和解决方法 Excel考勤公式-上班与休息日 完整学习LLM(五):Embedding是什么,为什么文本能变成向量 深度拆解 OpenCoWork:一个本地多智能体桌面平台的架构设计与实现 在影子里验证比较对象:随机指纹和哈希的数学原理 mysql备份恢复详解 HAProxy 学习总结 Mysql事物的持久性及原子性 应用内隐私信息被窥视?防窥保护自动感知一键防护 uni-app 实现视频聊天、屏幕分享,支持Android、HarmonyOS、iOS 做共享目录实时同步,踩过这些坑 华为公司发布半导体演进新范式 - “韬(τ)定律”(Tau Law) Linux时区修改为CST Go 语言入门学习笔记基础版 给热水器装上“电量显示”:用 Shelly Gen4 脚本实现零改装水量预测 踩坑实录:接口正常Feign调用字段值为空 耿同学学术打假,就是学术版《狂人日记》;学术打假,就是清扫垃圾 浙江事业编笔试上周出分!面试进入倒计时,该如何高效冲刺 - 里奥不吃奥利奥 FastApiAdmin 后端接口开发好了,前端管理界面怎么调用与显示? 我写了 50 个 Claude Code Skill 才发现,前 30 个都白写了 告别 "cd /var/log" !用 journalctl 统一掌控 Linux 日志 我用自己的微信聊天记录,微调了一个“数字分身” AI运动APP开发的常见问题集锦一 复盘梳理-如何深入并抽象 告别手动复制!公众号文章批量导出工具,极致提升内容运营效率 【学习笔记】《Python编程 从入门到实践》第2章:变量命名规则、字符串操作与数值类型详解 Docker--Docker简介及系统架构 别再瞎搞 AI 了!大厂AI业务落地的五个关键环节!(建议新手直接照搬) [MAF的Agent管道详解-01]塑智能体边界,从AIAgent抽象类开始 平台智能化到了分水岭:为什么配置代码化才是 AI Coding 的下一代接口 P.4文本统计工具 高光谱拼接算法(二)Harris 角点探测 - 哥布林学者
GoF设计模式——代理模式
咖啡八杯 · 2026-05-28 · via 博客园_首页

本文是【GoF设计模式】系列第7篇,更多内容欢迎关注公众号:咖啡八杯

image

前言

为什么需要代理模式?

有时候我们不能或不想直接访问某个对象。比如对象创建开销很大需要延迟加载,或者需要在访问前做权限检查,或者需要记录访问日志。直接在业务代码中掺杂这些逻辑会让代码臃肿且难以维护。

代理模式通过引入一个中间层,将这些控制逻辑从业务代码中分离出来。客户端代码不需要知道它是在和代理交互还是真实对象交互,两者可以透明替换。

概念

代理模式Proxy Pattern是一种结构型设计模式,核心思想是为一个对象提供一个替身(代理),以控制对这个对象的访问

代理对象和真实对象实现相同的接口,客户端通过代理间接访问真实对象,代理可以在调用前后添加额外的控制逻辑。

代理模式的主要角色有:

  • Subject(抽象主题):声明真实主题和代理共同实现的业务方法,客户端面向该接口编程
  • RealSubject(真实主题):定义代理所代表的真实对象,是客户端最终要访问的对象
  • Proxy(代理):持有对真实主题的引用,实现与真实主题相同的接口,在调用真实主题前后添加控制逻辑
    image

类图展示了静态结构,但对代理模式这种"调用拦截"的场景,时序图更能体现动态调用流程:

sequenceDiagram participant Client participant Proxy participant RealSubject Client->>Proxy: request() Proxy->>Proxy: 前置处理(权限/日志/延迟加载) Proxy->>RealSubject: request() RealSubject->>Proxy: 返回结果 Proxy->>Proxy: 后置处理(日志/缓存) Proxy->>Client: 返回结果

可以把代理理解为私人助理:老板(客户端)有事找某人(真实对象),先通过助理(代理)。助理可以在前面挡掉不重要的打扰(权限控制),也可以在事后记录行程(日志),老板全程不需要直接接触对方。这个比喻贯穿后面的实现章节,方便对照理解。

实现

代理模式的基本实现分为以下几个步骤:

  1. 定义抽象主题,一般是接口或抽象类,声明真实主题和代理对象实现的业务方法
  2. 定义真实主题,实现抽象主题中的具体业务
  3. 定义代理类,包含对 RealSubject 的引用,提供和真实主题相同的接口,在调用前后添加控制逻辑
  4. 客户端使用代理
// 抽象主题
interface Subject {
    // 声明同业务对象同名的方法
    public void request();
}

// 真实主题
class RealSubject implements Subject {
    public void request() {
        System.out.println("RealSubject request");
    }
}

// 代理类
class Proxy implements Subject {
    private RealSubject realSubject;

    @Override
    public void request() {
        // 访问真实主题之前:延迟加载
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        // 调用真实主题的方法
        realSubject.request();
        // 访问真实主题之后:可添加日志等逻辑
    }
}

总结

代理模式本质上是一层"中间人"——为真实对象提供一个替身,在调用前后添加控制逻辑。

什么时候用

  • 想控制对某个对象的访问(权限、延迟加载、缓存、日志)
  • 需要为远程对象提供本地代表
  • 引入第三方库或遗留代码,需要统一调用方式

什么时候不用

  • 接口差异巨大,代理会变得臃肿
  • 能修改真实对象源码且代价不大,直接修改更简单
  • 系统设计阶段就能定义接口规范,从源头统一即可

简单记忆

代理解决"控制访问"的问题,是给真实对象"加一层控制"。能改源码就改,改不了才用代理。

代理 vs 装饰器 vs 适配器 vs 外观:四个结构型模式都"包了一层对象",结构相似但意图不同:

模式 接口关系 核心意图
代理 目标接口 = 被包装对象接口 控制访问,附加访问前后逻辑
装饰器 目标接口 = 被包装对象接口 增强功能,接口不变
适配器 目标接口 ≠ 被包装对象接口 转换接口,让不兼容的类协同
外观 目标接口是新设计的 简化复杂子系统的调用

口诀对比:代理控访问,装饰增功能,适配改接口,外观简调用。

代理模式 vs 中介者模式

两者都引入"中间层",结构相似,但意图完全不同:

维度 代理模式 中介者模式
核心意图 控制对单个对象的访问 协调多个对象之间的交互
对象关系 客户端 → 代理 → 真实对象(单向委托) 多个同事对象 ↔ 中介者 ↔ 多个同事对象(多向协调)
封装内容 访问控制逻辑(权限、延迟加载、缓存) 对象间的交互规则、通信协议
客户端感知 客户端不知道真实对象存在 各同事对象知道中介者存在,但不直接知道其他同事
应用场景 Spring AOP、MyBatis Mapper、远程调用 MVC 框架的 Controller、聊天室服务器、GUI 事件分发

用例子说明

  • 代理模式:你要见 CEO,先通过秘书(代理)。秘书控制访问——过滤不重要的人、安排时间。你只和秘书打交道,CEO 对你透明。
  • 中介者模式:公司的各部门(销售、研发、财务)不直接相互沟通,所有协调通过行政部(中介者)。销售要研发资源,找行政部安排;财务要销售数据,找行政部转发。各部门知道行政部,但不直接依赖其他部门。

简单记忆

代理管"谁能动",中介者管"怎么联动"。代理是单对象的门禁,中介者是多对象的调度中心。

常见误区

  • 误区:代理模式 = 装饰器模式 → 意图不同:一个控访问,一个增功能
  • 误区:代理必须和真实对象同接口 → 对,这是代理的基本要求,否则就不是代理了
  • 误区:Spring @Transactional 在同一个类内部调用不生效 → 这是自调用绕过代理的经典坑,AOP 代理需要外部调用才能触发拦截逻辑

练习题目

门禁系统权限控制

题目描述:某科技园区的门禁系统管理着多个房间,每个房间有名称和最低访问权限等级。用户通过门禁终端访问房间,门禁终端作为代理,会检查用户的权限等级:

  • 用户权限 ≥ 房间要求的等级 → 放行,房间显示欢迎信息
  • 用户权限 < 房间要求的等级 → 拒绝,房间不会响应

请使用代理模式实现该门禁系统。其中:

  • Room(真实对象)拥有 enter() 方法,输出欢迎信息
  • RoomProxy(代理)在调用 enter() 前进行权限检查,只有通过才委托给真实对象

输入描述:第一行输入一个整数,表示用户的权限等级。第二行输入一个整数 N(1 ≤ N ≤ 20),表示要访问的房间数量。接下来 N 行,每行包含房间名称和该房间要求的最低权限等级,用空格分隔。

输出描述:对每个房间,通过代理访问后输出:

  • 权限足够:欢迎进入[房间名]
  • 权限不足:权限不足,无法进入[房间名]

输入示例

2
3
MeetingRoom 1
Lab 3
Office 2

输出示例

欢迎进入MeetingRoom
权限不足,无法进入Lab
欢迎进入Office

解题思路:代理类 RoomProxy 实现 Room 接口,内部持有用户权限。在 enter() 方法中先检查用户权限是否满足房间要求,满足则委托给 RealRoom.enter(),否则直接拒绝。这体现了代理模式的核心——代理在调用真实对象之前增加控制逻辑(权限校验),对客户端透明。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int qx = sc.nextInt();
        int n = sc.nextInt();
        RoomProxy proxy = new RoomProxy(qx);
        while (n-- > 0) {
            String name = sc.next();
            int roomQx = sc.nextInt();
            RealRoom room = new RealRoom(name, roomQx);
            proxy.setRoom(room);
            proxy.enter();
        }
    }
}

interface Room {
    public void enter();
}

class RealRoom implements Room {
    private String name;
    private int qx;

    public RealRoom(String name, int qx) {
        this.name = name;
        this.qx = qx;
    }

    public void enter() {
        System.out.println("欢迎进入" + this.name);
    }

    public String getName() {
        return this.name;
    }

    public int getQx() {
        return this.qx;
    }
}

class RoomProxy implements Room {
    private int qx;
    private RealRoom room;

    public RoomProxy(int qx) {
        this.qx = qx;
    }

    public void setRoom(RealRoom room) {
        this.room = room;
    }

    public void enter() {
        if (this.qx >= room.getQx()) {
            room.enter();
        } else {
            System.out.println("权限不足,无法进入" + room.getName());
        }
    }
}

扩展:实际项目中的代理模式

Spring AOP 的动态代理

Spring AOP 是代理模式最经典的应用。当 Bean 被 AOP 增强时,Spring 不会返回原始对象,而是返回一个代理对象。代理在方法调用前后插入切面逻辑(事务、日志、权限校验等),对调用方完全透明。

// 业务代码:完全感知不到代理的存在
@Service
public class OrderService {
    @Transactional  // 事务由代理自动管理
    public void createOrder(Order order) {
        orderDao.save(order);
        inventoryService.deduct(order.getProductId(), order.getQty());
    }
}

// Spring 内部:创建代理对象(JDK 动态代理或 CGLIB)
// 代理在 createOrder 前后自动开启/提交/回滚事务

关键点:Spring 默认对实现了接口的 Bean 使用 JDK 动态代理,对没有实现接口的 Bean 使用 CGLIB 代理。开发者只需写 @Transactional@Cacheable 等注解,代理负责增强逻辑的织入。

JDK 动态代理 vs CGLIB 对比

对比维度 JDK 动态代理 CGLIB
原理 实现接口,运行时生成 Proxy 类 继承目标类,生成子类
要求 目标必须实现接口 目标类不能是 final
Spring 默认场景 有接口的 Bean 无接口的 Bean
性能(高版本 JDK) 较高 较低

MyBatis 的 Mapper 代理

MyBatis 中我们只写 Mapper 接口,不需要写实现类,就能直接调用方法执行 SQL。这是因为 MyBatis 在启动时为每个 Mapper 接口生成了代理对象。

// 开发者只写接口
public interface UserMapper {
    @Select("SELECT * FROM user WHERE id = #{id}")
    User findById(Long id);
}

// 使用:直接注入接口,实际上是代理对象
@Service
public class UserService {
    @Autowired
    private UserMapper userMapper;  // 这是代理对象

    public User getUser(Long id) {
        return userMapper.findById(id);  // 代理拦截调用,执行 SQL
    }
}

// MyBatis 内部:MapperProxy 拦截接口方法调用
// 根据方法上的注解或 XML 映射,执行对应的 SQL 语句

关键点MapperProxy 实现了 InvocationHandler 接口,拦截所有方法调用,将接口方法映射为 SQL 执行。这是 JDK 动态代理的典型应用,让接口方法调用变成了数据库操作。

Java 动态代理实现 AOP 拦截器

Java 原生提供了 java.lang.reflect.ProxyInvocationHandler 接口,可以在运行时动态创建代理类。这是实现 AOP、拦截器等机制的基础。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 通用的日志拦截器
public class LogInterceptor implements InvocationHandler {
    private Object target;  // 被代理的真实对象

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

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("[LOG] 调用方法: " + method.getName());
        long start = System.currentTimeMillis();

        Object result = method.invoke(target, args);  // 调用真实对象

        long elapsed = System.currentTimeMillis() - start;
        System.out.println("[LOG] 方法 " + method.getName() + " 耗时: " + elapsed + "ms");
        return result;
    }

    // 工具方法:创建代理对象
    @SuppressWarnings("unchecked")
    public static <T> T createProxy(T target) {
        return (T) Proxy.newProxyInstance(
                target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new LogInterceptor(target));
    }
}

// 使用
interface UserService {
    public User findById(Long id);
}

class UserServiceImpl implements UserService {
    public User findById(Long id) {
        // 查询数据库...
        return new User(id, "张三");
    }
}

// 创建代理
UserService proxy = LogInterceptor.createProxy(new UserServiceImpl());
proxy.findById(1L);
// 输出:
// [LOG] 调用方法: findById
// [LOG] 方法 findById 耗时: 3ms

关键点Proxy.newProxyInstance 在运行时生成一个实现了指定接口的代理类,所有方法调用都会被转发到 InvocationHandler.invoke。这是 JDK 动态代理,要求目标类必须实现接口。如果目标类没有接口,需要使用 CGLIB(通过继承生成子类代理)。

Guava 的 Suppliers.memoize 懒加载代理

Guava 提供了 Suppliers.memoize,本质上是一个缓存代理:第一次调用时执行真实的 Supplier 获取值,后续调用直接返回缓存的结果。

import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;

// 模拟一个开销大的初始化操作
Supplier<DatabaseConnection> expensiveInit = new Supplier<DatabaseConnection>() {
    @Override
    public DatabaseConnection get() {
        System.out.println("正在建立数据库连接...");
        // 模拟耗时操作
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        return new DatabaseConnection("jdbc:mysql://localhost:3306/db");
    }
};

// 用 memoize 包装:第一次调用执行初始化,后续直接返回缓存
Supplier<DatabaseConnection> cached = Suppliers.memoize(expensiveInit);

// 第一次调用:触发初始化,耗时 3 秒
DatabaseConnection conn1 = cached.get();  // 输出:正在建立数据库连接...

// 第二次调用:直接返回缓存,瞬间完成
DatabaseConnection conn2 = cached.get();  // 无输出,直接返回

关键点Suppliers.memoize 内部用一个代理 Supplier 持有真实 Supplier 和缓存值,首次调用后将结果缓存,后续调用直接返回缓存。这是虚拟代理 + 缓存代理的结合,synchronized 保证线程安全。

Nginx 的反向代理

Nginx 作为反向代理服务器,是代理模式在架构层面的典型应用。客户端请求到达 Nginx,Nginx 将请求转发给后端的真实服务器,然后将响应返回给客户端。客户端只知道 Nginx 的地址,不知道后端服务器的存在。

# nginx.conf
http {
    upstream backend {
        server 192.168.1.10:8080 weight=3;  # 后端服务器1
        server 192.168.1.11:8080 weight=1;  # 后端服务器2
    }

    server {
        listen 80;

        location /api/ {
            proxy_pass http://backend;  # 代理转发
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

关键点:Nginx 代理在转发前后可以做很多事情——负载均衡(选择后端服务器)、权限校验(限制 IP)、缓存(静态资源直接返回)、日志记录(记录请求耗时)。这正是代理模式"控制访问"思想在架构层面的体现。