【设计模式】代理模式的实现方式与使用场景
1. 概述
代理模式是一种结构型设计模式,它通过创建一个代理对象来控制对另一个对象的访问,代理对象在客户端和目标对象之间充当了中介的角色,客户端不再直接访问目标对象,而是通过代理对象间接访问目标对象。
那在中间加一层代理的作用是什么呢?
有了中间这一层代理,我们就可以在目标对象方法调用前、调用后添加上一些额外的代码逻辑,在不改变目标对象的情况下,实现对目标对象的访问控制、功能增强、提高系统性能等功能。
代理模式按不同的实现方式分为静态代理和动态代理:
- 静态代理:在代码编写时显式的编写的代理类。
- 动态代理:在运行时动态生成代理类。
我们平时使用的更多的是动态代理,但不管是静态代理还是动态代理,最终生成的类关系是一致的,只是手工编写代码和框架生成代码的区别,所以在下面的实现方式内容中会优先讲到静态代理。
2.实现方式
代理模式有两种实现方式,一种是代理类与目标类实现相同的接口,另一种的代理类继承目标类并重写目标类的方法,两种方法没有太大的优劣之分,往往是互补的。
如果我们通过面向接口编程的原则设计的功能,就可以通过“实现接口”的方式来处理代理类,类关系图如下:

在上图中,代理类和目标类都实现了抽象的接口,代理对象通过关联关系持有了目标对象的引用,客户端同样通过关联持有了代理对象的引用。客户端可以向代理对象发起请求,代理对象收到请求后转发到目标对象中,同时在转发前后可以做一定的功能增强。
同样的,如果是我们在使用一些第三方的jar包,在使用到这些包的有可能目标类并没有实现一个具体的接口,这时候就可以通过继承的方式来实现。

在这种实现方式中,代理对象可以直接通过super调用目标对象的方法,看起来结构更为简单。
需要注意的是,由于代理对象需要请求目标对象的方法,所以代理对象一定要有目标对象方法的访问权限。
第一种方式中,代理对象通过关联的方式持有了目标对象,但是代理对象与目标对象不是父类与子类的关系、也可能并不在同一个包中,所以目标对象中被代理的方法一定是public方法,当然,目标对象实现了interface中的方法也只可能是public方法。
在第二种方式,由于是通过继承实现的,需要注意继承的权限,即:
- 父类不能用
final修饰 - 父类中需要重写的方法不能用
final修饰,不能是static方法,也不同由private修饰
总结一下,代理对象能代理目标对象的方法如下表:
| 实现方式 | 支持代理public | 支持protected | 支持default | 支持private |
|---|---|---|---|---|
| 接口实现 | 是 | 否 | 否 | 否 |
| 继承实现 | 是 | 是 | 是 | 否 |
熟悉Spring的同学可能已经发现了,这个表格和Spring AOP中的JDK代理与CGLIB代理支持的方法作用域是一致的。通过上面的分析,相信大家也理解了为什么Spring中的bean对象中的方法在非public修饰时,可能会导致AOP失效。
2.1.静态代理
接下来就是如何用代码来实现代理模式,用一个简单demo来体验一下静态代理,现在有一个UserSevice,我们需要在插入用户前开启事务,在插入完成后提交事务,代码如下:
- 用户接口和实现:
public interface UserService {void insertUser(); }public class UserServiceImpl implements UserService {@Overridepublic void insertUser() {System.out.println("查询用户");} } - 代理类:
public class UserServiceProxy implements UserService{private UserService userService;public UserServiceProxy(UserService userService) {this.userService = userService;}@Overridepublic void insertUser() {System.out.println("插入用户前开启事务");userService.insertUser();System.out.println("插入用户后提交事务");}}
然后我们模拟一下客户端,做一个测试:
public static void main(String[] args) {UserService userService = new UserServiceImpl();UserServiceProxy userServiceProxy = new UserServiceProxy(userService);userServiceProxy.insertUser();
}
插入用户前开启事务
插入用户
插入用户后提交事务
相信大家发现了,在客户端中既要创建用户服务的实例,也要创建代理对象实例,而更多的时候客户端可能并不关心请求的是代理对象还是实际的目标对象,这种情况可以结合工厂模式来处理。
以简单工厂为例(如果对简单工厂不熟悉可以看一下我的上一篇博客《什么场景可以考虑使用简单工厂模式》),写一个工厂:
public class UserServiceFactory {/*** 默认返回代理对象*/public static UserService getInstance() {return new UserServiceProxy(new UserServiceImpl());}
}
测试代码修改为:
public static void main(String[] args) {UserService userService = UserServiceFactory.getInstance();userService.insertUser();
}
插入用户前开启事务
插入用户
插入用户后提交事务
两次测试结果一致,且使用工厂后对客户端屏蔽了实现细节,写到这里相信大家已经有了熟悉感,没错,Spring的IOC容器底层就是一个工厂,它创建出的bean对象也是一个个的代理对象。
通过继承来实现代理模式也比较简单,这里就不过多的赘述了。
上面的代码看起来很容易就增强了目标对象中的方法,但如果需要代理的方法数量开始膨胀,需要代理的类也开始膨胀,例如我们有几十个向数据库插入数据的方法,每个方法我都得去写一遍代理逻辑,这就会导致开发和维护的成本成倍的上升,而且所有的代码都是高度类似的。
我希望把这些高度类似的代码都抽取出去,像模板一样,需要使用的地方就直接把模板套进去使用而不需要编写大量重复的代码,下面要说到的动态代理就能解决这个的问题
2.2.动态代理
我们梳理一下上面的静态代理类,这个代理类实现的功能是对Insert方法进行增强,自动开启事务和提交事务,我们在这个基础上将增强的逻辑提取出来做一个抽象,将匹配UserService抽象为匹配所有Insert操作。
然后是代码实现,在Java中可以使用JDK与CGLIB两种方式来实现动态代理,我们先看JDK实现的方式。
2.2.1. JDK的实现方式
JDK的代理方式要求目标对象一定是实现了某个interface,它是通过反射的方式来创建的代理对象,在java.lang.reflect有两个关键的类(接口):
InvocationHandler:定义了一个invoke方法,在这个方法中编写目标对象方法的增强逻辑。Proxy:可以通过目标对象与InvocationHandler创建一个代理对象。
按照上面所说的抽象方式,抽象出的事务处理器代码如下:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;public class TransactionHandlerByJdk implements InvocationHandler {/*** 目标对象*/private Object target;/*** 获取代理对象的方法** @param target 目标对象* @return 代理对象*/public Object getInstance(Object target) {this.target = target;Class<?> clazz = target.getClass();return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) {before();Object invoke = null;try {invoke = method.invoke(this.target, args);} catch (Exception e) {afterThrowing();}after();return invoke;}private void before() {System.out.println("开启事务");}private void after() {System.out.println("提交事务");}private void afterThrowing() {System.out.println("回滚事务");}}
写一个单元测试验证一下结果:
@Test
public void testTransaction() {UserService userService = (UserService) new TransactionHandlerByJdk().getInstance(new UserServiceImpl());userService.insertUser();
}
开启事务
插入用户
提交事务
在执行insert之前打一个断点可以观察到对应的userService是一个代理对象,在所有的框架执行过程中,只要我们看到$Proxy就可以断定它是一个代理对象。

2.2.2.CGLIB代理
如果目标类没有实现interface,可以考虑使用CGLIB来做动态代理,实现的方式也是类似的,在net.sf.cglib.proxy包下面也有两个重要的类(接口):
-
MethodInterceptor:定义了一个intercept方法,在这个方法中编写目标对象方法的增强逻辑。 -
Enhancer:可以通过目标对象与MethodInterceptor创建一个代理对象。 -
引入依赖:
<dependency><groupId>cglib</groupId><artifactId>cglib</artifactId><version>3.3.0</version> </dependency> -
编写抽象事务处理器
import net.sf.cglib.proxy.Enhancer; import net.sf.cglib.proxy.MethodInterceptor; import net.sf.cglib.proxy.MethodProxy;import java.lang.reflect.Method;public class TransactionHandlerByCglib implements MethodInterceptor {@SuppressWarnings("unchecked")public <T> T getInstance(Class<T> target) {Enhancer enhancer = new Enhancer();enhancer.setSuperclass(target);enhancer.setCallback(this);return (T) enhancer.create();}@Overridepublic Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {before();Object obj = null;try {obj = methodProxy.invokeSuper(o, objects);} catch (Exception e) {afterThrowing();}after();return obj;}private void before() {System.out.println("开启事务");}private void after() {System.out.println("提交事务");}private void afterThrowing() {System.out.println("回滚事务");} } -
测试
@Test public void testTransactionCglib() {UserService userService = new TransactionHandlerByCglib().getInstance(UserServiceImpl.class);userService.insertUser(); }最终测试的结果也是一样的:
开启事务
插入用户
提交事务
相对于静态代理而言,动态代理的优势在于只需要编写一些代理对象的处理器就可以动态的生成各种各样的代理对象。
在上面的例子中,如果又新增了一个部门服务,只需要在客户端传入对应的目标对象(部门服务对象)就可以享受到自动管理事务的待遇了,不需要修改代理相关的任何的代码,这是静态代理所不具备的优势,这也是我们经常遇到的代理模式是动态代理的原因。
3.使用场景
由于静态代理在使用的时候,需要针对每个对象都创建一个对应的代理对象, 非常繁琐,在实际的项目中运用的并不是太多,一般都是选择使用动态代理模式,主要考虑两种场景:
- 需要将业务代码与非业务代码的分离。
- 多个方法都有相同的操作时,做统一处理。
实际上在大多数时候是同时满足两种场景的,例如下面这些场景:
- 鉴权:例如针对需要用户权限验证的接口,每个接口在调用前都需要验证当前登录人信息。
- 监控:例如在重要流程接口运行时出现了异常,需要在异常出现时给维护人员发送告警消息。
- 日志:例如接口请求日志,在关键接口中需要记录访问人、访问IP、请求参数、响应参数等信息。
- 统计:例如接口的访问次数统计。
- 事务:数据库的事务开启、提交、回滚操作与业务代码分离。
- ……
4.总结
本篇主要讲述的是代理模式的实现方式与使用场景,先介绍了代理模式的概念和作用,然后从静态代理开始讲述了代理模式的实现方式,其中静态代理的使用频率并不高,动态代理则相反,使用频率非常高,需要重点掌握。
之所以花了一部分篇幅讲解静态代理,主要是能够直观的感受到代理模式的类结构,后续动态代理生成的代码与静态代理的也大同小异。
我们在鉴权、监控、统计、日志、事务等多种场景中都可以使用动态代理模式。在使用代理模式的时候需要注意接口实现与继承实现两种方式的区别及注意事项,重点是下面这个表:
| 实现方式 | 支持代理public | 支持protected | 支持default | 支持private |
|---|---|---|---|---|
| 接口实现 | 是 | 否 | 否 | 否 |
| 继承实现 | 是 | 是 | 是 | 否 |
至于性能上,两种实现动态代理的方式在性能上可能有细微的差异,但在实际应用中并不明显,在选择动态代理方式时,应该根据具体的需求和场景来决定使用哪种方式。
相关文章:
【设计模式】代理模式的实现方式与使用场景
1. 概述 代理模式是一种结构型设计模式,它通过创建一个代理对象来控制对另一个对象的访问,代理对象在客户端和目标对象之间充当了中介的角色,客户端不再直接访问目标对象,而是通过代理对象间接访问目标对象。 那在中间加一层代理…...
医学图像的图像处理、分割、分类和定位-1
一、说明 本报告全面探讨了应用于医学图像的图像处理和分类技术。开展了四项不同的任务来展示这些方法的多功能性和有效性。任务 1 涉及读取、写入和显示 PNG、JPG 和 DICOM 图像。任务 2 涉及基于定向变化的多类图像分类。此外,我们在任务 3 中包括了胸部 X 光图像…...
【51单片机】外部中断
0、前言 参考:普中 51 单片机开发攻略 第16章 及17章 1、硬件 2、软件 #include <reg52.h> #include <intrins.h> #include "delayms.h"typedef unsigned char u8; typedef unsigned int u16;sbit led P2^0; sbit key3 P3^2;//外部中断…...
fastapi框架
fastapi框架 fastapi,一个用于构建 API 的现代、快速(高性能)的异步web框架。 fastapi是建立在Starlette和Pydantic基础上的 Pydantic是一个基于Python类型提示来定义数据验证、序列化和文档的库。Starlette是一种轻量级的ASGI框架/工具包…...
2023 年顶级前端工具
谁不喜欢一个好的前端工具?在本综述中,您将找到去年流行的有用的前端工具,它们将帮助您加快开发工作流程。让我们深入了解一下! 在过去的 12 个月里,我在我的时事通讯 Web Tools Weekly 中分享了数百种工具。我为前端…...
html 会跳舞的时间动画特效
下面是是代码: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns"http://www.w3.org/1999/xhtml"> <head> <meta h…...
微信AR实现识别手部展示glb模型
1.效果 2.微信小程序手势识别只支持以下几个动作,和识别点位,官方文档 因为AR识别手部一直在识别,所以会出现闪动问题。可以将微信开发者调试基础库设置到3.3.2以上,可能要稳定一些 3.3.代码展示,我用的是微信官方文…...
MYSQL自连接、子查询
自连接: # board表 mysql> select * from board; --------------------------------- | id | name | intro | parent_id | --------------------------------- | 1 | 后端 | NULL | NULL | | 2 | 前端 | NULL | NULL | | 3 | 移…...
docker搭建hbase 全部流程(包含本地API访问)
一、使用docker下载并安装hbase 1、搜索:docker search hbase 2、下载:docker pull harisekhon/hbase(一定要下载这个,下面都是围绕此展开的) 3、启动容器: docker run -d -p 2181:2181 -p 16000:16000…...
Mybatis之关联
一、一对多关联 eg:一个用户对应多个订单 建表语句 CREATE TABLE t_customer (customer_id INT NOT NULL AUTO_INCREMENT, customer_name CHAR(100), PRIMARY KEY (customer_id) ); CREATE TABLE t_order ( order_id INT NOT NULL AUTO_INCREMENT, order_name C…...
Labview实现用户界面切换的几种方式---通过VI间相互调用
在做用户界面时我们的程序往往面对的对象是程序使用者,复杂程序如果放在同一个页面中,往往会导致程序冗长卡顿,此时通过多个VI之间的切换就可以实现多个界面之间的转换,也会显得程序更加的高大上。 本文所有程序均可下载ÿ…...
点云从入门到精通技术详解100篇-基于点云和图像融合的智能驾驶目标检测(中)
目录 2.1.2 数据源选型分析 2.2 环境感知系统分析 2.2.1 传感器布置方案分析...
Apache-iotdb物联网数据库的安装及使用
一、简介 >Apache IoTDB (Database for Internet of Things) is an IoT native database with high performance for data management and analysis, deployable on the edge and the cloud. Due to its light-weight architecture, high performance and rich feature set…...
项目管理流程
优质博文 IT-BLOG-CN 一、简介 项目是为提供某项独特产品【独特指:创造出与以往不同或者多个方面与以往有所区别产品或服务,所以日复一日重复的工作就不属于项目】、服务或成果所做的临时性【临时性指:项目有明确的开始时间和明确的结束时间,不会无限期…...
0004.电脑开机提示按F1
常用的电脑主板不知道什么原因,莫名其妙的启动不了了。尝试了很多方法,没有奏效。没有办法我就只能把硬盘拆了下来,装到了另一台电脑上面。但是开机以后却提示F1,如下图: 根据上面的提示,应该是驱动有问题…...
中国电子学会2022年12月份青少年软件编程Scratch图形化等级考试试卷一级真题(含答案)
一、单选题(共25题,共50分) 1. 小明想在开始表演之前向大家问好并做自我介绍,应运行下列哪个程序?(2分) A. B. C. D. 2. 舞台有两个不同的背景,小猫角色的哪个积木能够切换舞台背景?(2分) A. B. C. D. 3. …...
C语言第二弹---C语言基本概念(下)
✨个人主页: 熬夜学编程的小林 💗系列专栏: 【C语言详解】 【数据结构详解】 C语言基本概念 1、字符串和\02、转义字符3、语句和语句分类3.1、空语句3.2、表达式语句3.3、函数调⽤语句3.4、复合语句3.5、控制语句 4、注释4.1、注释的两种形…...
Java 基础面试题 String(一)
Java 基础面试题 String(一) 文章目录 Java 基础面试题 String(一)String、StringBuffer、StringBuilder 的区别?String 为什么是不可变的?字符串拼接用“” 还是 StringBuilder? 文章来自Java Guide 用于学习如有侵…...
QT中QApplication对象有且只有一个
QT中QApplication对象有且只有一个 QApplication对象 QApplication对象 QApplication是应用程序对象 #include <QApplication> int main(int argc,char* argv[]); {//a对象在一个程序中有且只有一个,QT中要求必须有一个QApplication a(argc,argv…...
HTML CSS 发光字头特效
效果展示: 代码: <html><head> </head><style>*{margin: 0;padding: 0;}body {text-align: center;}h1{/* border: 3px solid rgb(201, 201, 201); */margin-bottom: 20px;}.hcqFont {position: relative;letter-spacing: 0.07…...
【Oracle APEX开发小技巧12】
有如下需求: 有一个问题反馈页面,要实现在apex页面展示能直观看到反馈时间超过7天未处理的数据,方便管理员及时处理反馈。 我的方法:直接将逻辑写在SQL中,这样可以直接在页面展示 完整代码: SELECTSF.FE…...
k8s从入门到放弃之Ingress七层负载
k8s从入门到放弃之Ingress七层负载 在Kubernetes(简称K8s)中,Ingress是一个API对象,它允许你定义如何从集群外部访问集群内部的服务。Ingress可以提供负载均衡、SSL终结和基于名称的虚拟主机等功能。通过Ingress,你可…...
跨链模式:多链互操作架构与性能扩展方案
跨链模式:多链互操作架构与性能扩展方案 ——构建下一代区块链互联网的技术基石 一、跨链架构的核心范式演进 1. 分层协议栈:模块化解耦设计 现代跨链系统采用分层协议栈实现灵活扩展(H2Cross架构): 适配层…...
在QWebEngineView上实现鼠标、触摸等事件捕获的解决方案
这个问题我看其他博主也写了,要么要会员、要么写的乱七八糟。这里我整理一下,把问题说清楚并且给出代码,拿去用就行,照着葫芦画瓢。 问题 在继承QWebEngineView后,重写mousePressEvent或event函数无法捕获鼠标按下事…...
莫兰迪高级灰总结计划简约商务通用PPT模版
莫兰迪高级灰总结计划简约商务通用PPT模版,莫兰迪调色板清新简约工作汇报PPT模版,莫兰迪时尚风极简设计PPT模版,大学生毕业论文答辩PPT模版,莫兰迪配色总结计划简约商务通用PPT模版,莫兰迪商务汇报PPT模版,…...
STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...
计算机基础知识解析:从应用到架构的全面拆解
目录 前言 1、 计算机的应用领域:无处不在的数字助手 2、 计算机的进化史:从算盘到量子计算 3、计算机的分类:不止 “台式机和笔记本” 4、计算机的组件:硬件与软件的协同 4.1 硬件:五大核心部件 4.2 软件&#…...
(一)单例模式
一、前言 单例模式属于六大创建型模式,即在软件设计过程中,主要关注创建对象的结果,并不关心创建对象的过程及细节。创建型设计模式将类对象的实例化过程进行抽象化接口设计,从而隐藏了类对象的实例是如何被创建的,封装了软件系统使用的具体对象类型。 六大创建型模式包括…...
Chromium 136 编译指南 Windows篇:depot_tools 配置与源码获取(二)
引言 工欲善其事,必先利其器。在完成了 Visual Studio 2022 和 Windows SDK 的安装后,我们即将接触到 Chromium 开发生态中最核心的工具——depot_tools。这个由 Google 精心打造的工具集,就像是连接开发者与 Chromium 庞大代码库的智能桥梁…...
Java详解LeetCode 热题 100(26):LeetCode 142. 环形链表 II(Linked List Cycle II)详解
文章目录 1. 题目描述1.1 链表节点定义 2. 理解题目2.1 问题可视化2.2 核心挑战 3. 解法一:HashSet 标记访问法3.1 算法思路3.2 Java代码实现3.3 详细执行过程演示3.4 执行结果示例3.5 复杂度分析3.6 优缺点分析 4. 解法二:Floyd 快慢指针法(…...
