【设计模式】代理模式的实现方式与使用场景

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 {
        @Override
        public void insertUser() {
            System.out.println("查询用户");
        }
    }
    
  • 代理类:
public class UserServiceProxy implements UserService{

    private UserService userService;

    public UserServiceProxy(UserService userService) {
        this.userService = userService;
    }

    @Override
    public 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();
}

插入用户前开启事务
插入用户
插入用户后提交事务

两次测试结果一致,且使用工厂后对客户端屏蔽了实现细节,写到这里相信大家已经有了熟悉感,没错,SpringIOC容器底层就是一个工厂,它创建出的bean对象也是一个个的代理对象

通过继承来实现代理模式也比较简单,这里就不过多的赘述了。


上面的代码看起来很容易就增强了目标对象中的方法,但如果需要代理的方法数量开始膨胀,需要代理的类也开始膨胀,例如我们有几十个向数据库插入数据的方法,每个方法我都得去写一遍代理逻辑,这就会导致开发和维护的成本成倍的上升,而且所有的代码都是高度类似的

我希望把这些高度类似的代码都抽取出去,像模板一样,需要使用的地方就直接把模板套进去使用而不需要编写大量重复的代码,下面要说到的动态代理就能解决这个的问题

2.2.动态代理

我们梳理一下上面的静态代理类,这个代理类实现的功能是Insert方法进行增强,自动开启事务和提交事务,我们在这个基础上将增强的逻辑提取出来做一个抽象,将匹配UserService抽象为匹配所有Insert操作。

然后是代码实现,在Java中可以使用JDKCGLIB两种方式来实现动态代理,我们先看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);
    }

    @Override
    public 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();
        }
    
        @Override
        public 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
接口实现
继承实现

至于性能上,两种实现动态代理的方式在性能上可能有细微的差异,但在实际应用中并不明显,在选择动态代理方式时,应该根据具体的需求和场景来决定使用哪种方式。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/340171.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

抓包工具Fidder

介绍 Fiddler是强大的抓包工具&#xff0c;它的原理是以web代理服务器的形式进行工作的&#xff0c;代理地址&#xff1a;127.0.0.1&#xff0c;默认端口号:8888。代理就是在客户端和服务器之间设置一道官咖&#xff0c;客户端将请求数据发送出去之后&#xff0c;代理服务器会…

为什么电脑降价了?

周末&#xff0c;非常意外地用不到3000元买到了一款2023年度发布的华为笔记本I5,16G,500G&#xff0c;基本是主流配置&#xff0c;我非常意外&#xff0c;看了又看&#xff0c;不是什么Hwawii&#xff0c;或者Huuawe。然后也不是二手。为什么呢&#xff1f;因为在ALU和FPU之外&…

一零七七、将Hexo cl Hexo g Hexo s通过systemctl命令管理

背景&#xff1a; 服务器需要执行hexo s来运行项目&#xff0c;但这个命令是基于前台的&#xff0c;故想直接嫁接在systemctl命令基础上来控制环境&#xff1a; Centos 8 前置环境就不说了,Hexo安装好&#xff0c;起码装完自己得先看hexo命令生效没&#xff0c;前置环境做好后…

项目篇:基于UDP通信模型的网络聊天室

思维导图 基于UDP通信模型的网络聊天室 消息分类及数据包结构 服务器端 #include <head.h> #define SER_PORT 8888 #define SER_IP "192.168.232.133" typedef struct mb {struct sockaddr_in cin;char name[20];struct mb *next; }*member; //群发消息 int …

【英文干货】【Word_Search】找单词游戏(第1天)

本期主题&#xff1a;Mindfulness&#xff08;意识力&#xff09; 本期单词&#xff1a; Awareness 意识 Breathing 呼吸 Calm 平静的 De-Stress 减压 Feelings 感受&#xff0c;情感 Inspection 调查 Meditation 冥想 Peace 和平 Quiet 安静的 Recollection 回忆 R…

当世界加速离你而去

当世界加速离你而去 会不会这个标题显的太悲观&#xff0c;也可能是耳机里正在放着To Be Frank的原因。 对于阳历跨年我是没有太多的感觉&#xff0c;而且跨年夜忙着约会&#xff0c;所以2023年的跨年文章今天才出来。 一年的时间一晃就过了。2022年12月9日时候彻底结束了风控…

ELK 分离式日志(1)

目录 一.ELK组件 ElasticSearch&#xff1a; Kiabana&#xff1a; Logstash&#xff1a; 可以添加的其它组件&#xff1a; ELK 的工作原理&#xff1a; 二.部署ELK 节点都设置Java环境: 每台都可以部署 Elasticsearch 软件&#xff1a; 修改elasticsearch主配置文件&…

FOR XML PATH 函数与同一分组下的字符串拼接

FOR XML PATH 简单介绍 FOR XML PATH 语句是将查询结果集以XML形式展现&#xff0c;通常情况下最常见的用法就是将多行的结果&#xff0c;拼接展示在同一行。 首先新建一张测试表并插入数据&#xff1a; CREATE TABLE #Test (Name varchar(70),Hobby varchar(70) );insert #T…

EHS管理系统为何需要物联网的加持?

EHS是Environment、Health、Safety的缩写&#xff0c;是从欧美企业引进的管理体系&#xff0c;在国外也被称为HSE。EHS是指健康、安全与环境一体化的管理。 而在国内&#xff0c;整个EHS市场一共被分成三类&#xff1b; 一类是EHS管培体系&#xff0c;由专门的EHS机构去为公司…

Watch(监视器)+(综合案例)

Watch侦听器&#xff08;监视器&#xff09; 作用&#xff1a;监视数据变化&#xff0c;执行一些业务逻辑或异步操作 语法&#xff1a; ①简单写法 → 简单类型数据&#xff0c;直接监视 ②完整写法 → 添加额外配置项 ①简单写法 <!DOCTYPE html> <html lang"…

CLion调试Nodejs源码

【环境】 macOS node-v20.11.0源码 CLion 2023.3.2 【1】下载源码 https://nodejs.org/en/download/ 【2】编译源码 解压后的目录如下 进入解压后的目录进行编译 ./configure --debug make -C out BUILDTYPEDebug -j 4需要好久… 编译成功之后在node-v20.11.0目录下会有一个…

DALL·E 3功能:开启创意无限的新纪元

在人工智能的黄金时代&#xff0c;MidTool以其DALLE 3功能引领了一个全新的创意革命。这项技术不仅仅是一个简单的图像生成工具&#xff0c;它是一种将想象力转化为视觉现实的魔法。在这篇文章中&#xff0c;我们将深入探讨MidTool的DALLE 3功能&#xff0c;并揭示它如何成为艺…

三、MySQL之创建和管理表

一、基础知识 1.1 一条数据存储的过程 存储数据是处理数据的第一步 。只有正确地把数据存储起来,我们才能进行有效的处理和分析。否则,只 能是一团乱麻,无从下手。 在 MySQL 中, 一个完整的数据存储过程总共有 4 步,分别是创建数据库、确认字段、创建数据表、插入数据。 …

EasyDarwin计划新增将各种流协议(RTSP、RTMP、HTTP、TCP、UDP)、文件转推RTMP到其他视频直播平台,支持转码H.264、文件直播推送

之前我们尝试做过EasyRTSPLive&#xff08;将RTSP流转推RTMP&#xff09;和EasyRTMPLive&#xff08;将各种RTSP/RTMP/HTTP/UDP流转推RTMP&#xff0c;这两个服务在市场上都得到了比较多的好评&#xff0c;其中&#xff1a; 1、EasyRTSPLive用的是EasyRTSPClient取流&#xff…

技术浪潮下的程序员职业困境:一对谷歌工程师夫妻的悲剧启示

目录 前言1 裁员潮下的程序员1.1 技术变革带来的裁员潮1.2 程序员职业危机&#xff1a;技能匮乏成为致命伤 2 一对谷歌工程师夫妻的悲剧2.1 事件经过2.2 心理压力和职业困境的交织 3 技术浪潮下的程序员职业适应策略3.1 持续学习与技能更新3.2 多元化技能与职业规划3.3 职业心理…

EasyExcelFactory 导入导出功能的实战使用

EasyExcelFactory 导入导出功能的实战使用分享&#xff1a; 1、jar包引入 <!-- 阿里巴巴Excel处理--><dependency><groupId>com.alibaba</groupId><artifactId>easyexcel</artifactId><version>3.0.6</version></dependen…

2.机器学习-K最近邻(k-Nearest Neighbor,KNN)分类算法原理讲解

2️⃣机器学习-K最近邻&#xff08;k-Nearest Neighbor&#xff0c;KNN&#xff09;分类算法原理讲解 个人简介一算法概述二算法思想2.1 KNN的优缺点 三实例演示3.1电影分类3.2使用KNN算法预测 鸢(yuan)尾花 的种类3.3 预测年收入是否大于50K美元 个人简介 &#x1f3d8;️&…

华为欧拉操作系统结合内网穿透实现固定公网地址SSH远程连接

文章目录 1. 本地SSH连接测试2. openEuler安装Cpolar3. 配置 SSH公网地址4. 公网远程SSH连接5. 固定连接SSH公网地址6. SSH固定地址连接测试 欧拉操作系统(openEuler, 简称“欧拉”)是面向数字基础设施的操作系统,支持服务器、云计算、边缘openEuler是面向数字基础设施的操作系…

多线程实例练习题~

本篇文章主要是用来巩固多线程的简单应用&#xff0c;如果你已经学习了多线程的有关知识&#xff0c;想要巩固&#xff0c;那不妨拿下面几道题来考验一下自己吧&#xff01; 案例1&#xff1a;电影院售票(难度指数&#xff1a;一颗星) 题目&#xff1a;一共有1000张电影票&…

8.3 Springboot整合Redis 之Jedis方式

文章目录 前言一、Maven依赖二、新增子Module:tg-book-redis三、Jedis配置类3.1 Jedis连接池核心配置说明四、Jedis 工具类五、新增controller测试前言 Jedis是Redis官方推荐的Java客户端连接工具,用法非常简单,Jedis的API与Redis的API可以说是一模一样,所以非常有利于熟悉…
最新文章