【设计模式】代理模式的实现方式与使用场景
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…...
Java 语言特性(面试系列2)
一、SQL 基础 1. 复杂查询 (1)连接查询(JOIN) 内连接(INNER JOIN):返回两表匹配的记录。 SELECT e.name, d.dept_name FROM employees e INNER JOIN departments d ON e.dept_id d.dept_id; 左…...
java_网络服务相关_gateway_nacos_feign区别联系
1. spring-cloud-starter-gateway 作用:作为微服务架构的网关,统一入口,处理所有外部请求。 核心能力: 路由转发(基于路径、服务名等)过滤器(鉴权、限流、日志、Header 处理)支持负…...

css实现圆环展示百分比,根据值动态展示所占比例
代码如下 <view class""><view class"circle-chart"><view v-if"!!num" class"pie-item" :style"{background: conic-gradient(var(--one-color) 0%,#E9E6F1 ${num}%),}"></view><view v-else …...

linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...

现代密码学 | 椭圆曲线密码学—附py代码
Elliptic Curve Cryptography 椭圆曲线密码学(ECC)是一种基于有限域上椭圆曲线数学特性的公钥加密技术。其核心原理涉及椭圆曲线的代数性质、离散对数问题以及有限域上的运算。 椭圆曲线密码学是多种数字签名算法的基础,例如椭圆曲线数字签…...
Java入门学习详细版(一)
大家好,Java 学习是一个系统学习的过程,核心原则就是“理论 实践 坚持”,并且需循序渐进,不可过于着急,本篇文章推出的这份详细入门学习资料将带大家从零基础开始,逐步掌握 Java 的核心概念和编程技能。 …...

多种风格导航菜单 HTML 实现(附源码)
下面我将为您展示 6 种不同风格的导航菜单实现,每种都包含完整 HTML、CSS 和 JavaScript 代码。 1. 简约水平导航栏 <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…...
什么?连接服务器也能可视化显示界面?:基于X11 Forwarding + CentOS + MobaXterm实战指南
文章目录 什么是X11?环境准备实战步骤1️⃣ 服务器端配置(CentOS)2️⃣ 客户端配置(MobaXterm)3️⃣ 验证X11 Forwarding4️⃣ 运行自定义GUI程序(Python示例)5️⃣ 成功效果
springboot整合VUE之在线教育管理系统简介
可以学习到的技能 学会常用技术栈的使用 独立开发项目 学会前端的开发流程 学会后端的开发流程 学会数据库的设计 学会前后端接口调用方式 学会多模块之间的关联 学会数据的处理 适用人群 在校学生,小白用户,想学习知识的 有点基础,想要通过项…...

群晖NAS如何在虚拟机创建飞牛NAS
套件中心下载安装Virtual Machine Manager 创建虚拟机 配置虚拟机 飞牛官网下载 https://iso.liveupdate.fnnas.com/x86_64/trim/fnos-0.9.2-863.iso 群晖NAS如何在虚拟机创建飞牛NAS - 个人信息分享...