【Java设计模式 设计模式与范式】结构型模式 二:代理模式

本文涉及的产品
云数据库 RDS MySQL,集群版 2核4GB 100GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 【Java设计模式 设计模式与范式】结构型模式 二:代理模式

本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是代理模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:

  • 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
  • 模式结构:包含模式的角色定义及调用关系以及其模版代码
  • 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
  • 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
  • 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
  • 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构

接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构

模式档案

在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,买房不需要直接找业主谈,找房产中介谈即可(贝壳打钱),再比如下载Docker的镜像由于网速限制不直接从DockerHub上下载,直接从阿里云镜像加速站点下载(阿里打钱)。在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间;因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等

模式定义:由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介

模式特点:主要特点是能最大限度的保护目标对象扩展目标对象的功能,以及解耦目标对象与访问对象

解决什么问题:主要解决在软件系统中直接访问目标对象时带来的问题。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层

优点:该模式的主要优点如下:

  • 代理模式在客户端与目标对象之间起到一个中介作用保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能
  • 代理模式能将访问对象与目标对象分离,在一定程度上降低了系统的耦合度,增加了程序的可扩展性

缺点:该模式的主要缺点如下:

  • 代理模式会造成系统设计中类的数量增加
  • 在客户端和目标对象之间增加一个代理对象,会造成请求处理速度变慢
  • 增加了系统的复杂度

使用场景: 该模式通常适用于以下场景。

  • 远程代理,这种方式通常是为了隐藏目标对象存在于不同地址空间的事实,方便客户端访问。例如,用户申请某些网盘空间时,会在用户的文件系统中建立一个虚拟的硬盘,用户访问虚拟硬盘时实际访问的是网盘空间。
  • 虚拟代理,这种方式通常用于要创建的目标对象开销很大时。例如,下载一幅很大的图像需要很长时间,因某种计算比较复杂而短时间无法完成,这时可以先用小比例的虚拟代理替换真实的对象,消除用户对服务器慢的感觉。
  • 安全代理,这种方式通常用于控制不同种类客户对真实对象的访问权限。
  • 智能指引,主要用于调用目标对象时,代理附加一些额外的处理功能。例如,增加计算真实对象的引用次数的功能,这样当该对象没有被引用时,就可以自动释放它。
  • 延迟加载,指为了提高系统的性能,延迟对目标的加载。例如,Hibernate 中就存在属性的延迟加载和关联表的延时加载。

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。代理模式在平时的开发经常被用到,常用在业务系统中开发一些非功能性需求,比如:监控、统计、鉴权、限流、事务、幂等、日志

模式结构

代理模式的结构比较简单,主要是通过定义一个继承抽象主题的代理来包含真实主题,从而实现对真实主题的访问,下面来分析其基本结构和实现方法,主要有以下三个角色:

  • 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

角色的相互调用关系如下图所示:

在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。根据代理的创建时期,代理模式分为静态代理和动态代理。

  • 静态代理:创建代理类或特定工具自动生成源代码再对其编译,在程序运行前代理类的 .class 文件就已经存在了
  • 动态代理:在程序运行时,运用反射机制动态创建而成

关于静态代理和动态代理在我的这篇Blog中详细介绍过:【Spring学习笔记 六】静态/动态代理实现机制

模式实现

下面通过代码结构来看下如何实现一个代理模式:

1 抽象主题

//抽象主题
interface Subject {
    void request();
}

2 真实主题

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

3 代理类

//代理对象
@AllArgsConstructor
class Proxy implements Subject {
    private RealSubject realSubject;
    public void request() {
        preRequest();
        realSubject.request();
        postRequest();
    }
    public void preRequest() {
        System.out.println("访问真实主题之前的预处理!");
    }
    public void postRequest() {
        System.out.println("访问真实主题之后的后续处理!");
    }
}

客户端调用如下

public class ProxyTest {
    public static void main(String[] args) {
        RealSubject realSubject = new RealSubject();
        Proxy proxy = new Proxy(realSubject);
        //客户端不直接访问对象的方法,而是通过代理对象访问,并在增加了附加的处理逻辑
        proxy.request();
    }
}

打印结果如下:

模式实践

我们来看两个代理模式的实践:设计一个数据库连接辅助工具 以及 设计一个性能计数器

设计一个数据库连接辅助工具

我们想要在创建MySQL数据库连接时做如下两个动作:在连接前判断当前登录人是否有权限创建连接,在获取连接后加一个日志,输出确实创建连接成功了

package com.example.designpattern.proxy;
public class DbConnect {
    public static void main(String[] args) {
        MySqlConnectionCreateImpl mySqlConnectionCreateImpl = new MySqlConnectionCreateImpl();
        SqlConnectionProxy proxy = new SqlConnectionProxy(mySqlConnectionCreateImpl);
        proxy.createMySqlConnection("TML");
    }
}
interface MySqlConnectionCreate {
    void createMySqlConnection(String dbName);
}
class MySqlConnectionCreateImpl implements MySqlConnectionCreate {
    @Override
    public void createMySqlConnection(String dbName) {
        System.out.println("创建MySQL数据库连接成功,连接到:" + dbName);
    }
}
class SqlConnectionProxy implements MySqlConnectionCreate {
    private MySqlConnectionCreateImpl mySqlConnectionCreateImpl;
    public SqlConnectionProxy(MySqlConnectionCreateImpl mySqlConnectionCreateImpl) {
        this.mySqlConnectionCreateImpl = mySqlConnectionCreateImpl;
    }
    @Override
    public void createMySqlConnection(String dbName) {
        this.doSomethingBefore();
        mySqlConnectionCreateImpl.createMySqlConnection(dbName);
        this.doSomethingAfter();
    }
    private void doSomethingBefore() {
        System.out.println("连接数据库前的额外操作:权限验证");
    }
    private void doSomethingAfter() {
        System.out.println("连接数据库前的额外操作:日志记录");
    }
}

调用结果如下:

设计一个性能计数器

假设我们有一个MetricsCollector 类,用来收集接口请求的原始数据,比如访问时间、处理时长等。我们最直接的想法就是:

public class UserController {
  //...省略其他属性和方法...
  private MetricsCollector metricsCollector; // 依赖注入
  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    // ... 省略login逻辑...
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    //...返回UserVo数据...
  }
  public UserVo register(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    // ... 省略register逻辑...
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    //...返回UserVo数据...
  }
}

上面的写法有两个问题。第一,MetricsCollector 代码侵入到业务代码中,跟业务代码高度耦合。如果未来需要替换这个框架,那替换的成本会比较大。第二,收集接口请求的代码跟业务代码无关,本就不应该放到一个类中。业务类最好职责更加单一,只聚焦业务处理。于是我们用代理来解决这个问题。

1 代理类分离业务代码和性能计数器

为了将框架代码和业务代码解耦,代理模式就派上用场了。代理类 UserControllerProxy 和原始类 UserController 实现相同的接口 IUserController。UserController 类只负责业务功能。代理类 UserControllerProxy 负责在业务代码执行前后附加其他逻辑代码,并通过委托的方式调用原始类来执行业务代码

抽象主题

public interface IUserController {
  UserVo login(String telephone, String password);
  UserVo register(String telephone, String password);
}

真实主题

public class UserController implements IUserController {
  //...省略其他属性和方法...
  @Override
  public UserVo login(String telephone, String password) {
    //...省略login逻辑...
    //...返回UserVo数据...
  }
  @Override
  public UserVo register(String telephone, String password) {
    //...省略register逻辑...
    //...返回UserVo数据...
  }
}

代理类

public class UserControllerProxy implements IUserController {
  private MetricsCollector metricsCollector;
  private UserController userController;
  public UserControllerProxy(UserController userController) {
    this.userController = userController;
    this.metricsCollector = new MetricsCollector();
  }
  @Override
  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    // 委托
    UserVo userVo = userController.login(telephone, password);
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    return userVo;
  }
  @Override
  public UserVo register(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();
    UserVo userVo = userController.register(telephone, password);
    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);
    return userVo;
  }
}

客户端调用

//UserControllerProxy使用举例
//因为原始类和代理类实现相同的接口,是基于接口而非实现编程
//将UserController类对象替换为UserControllerProxy类对象,不需要改动太多代码
IUserController userControllerProxy = new UserControllerProxy(new UserController());
userControllerProxy.login("188****6234","tttttt");

以上代码还有两个问题:

  • 一方面,我们需要在代理类中,将原始类中的所有的方法,都重新实现一遍,并且为每个方法都附加相似的代码逻辑。
  • 另一方面,如果要添加的附加功能的类有不止一个,我们需要针对每个类都创建一个代理类。如果有 50 个要添加附加功能的原始类,那我们就要创建 50 个对应的代理类。这会导致项目中类的个数成倍增加,增加了代码维护成本。并且,每个代理类中的代码都有点像模板式的“重复”代码,也增加了不必要的开发成本。

那么该怎么处理这个问题呢?

2 动态代理降低代码维护成本

那这个问题怎么解决呢?我们可以使用动态代理来解决这个问题。所谓动态代理(Dynamic Proxy),就是我们不事先为每个原始类编写代理类,而是在运行的时候,动态地创建原始类对应的代理类,然后在系统中用代理类替换掉原始类。那如何实现动态代理呢?

代理工厂

public class MetricsCollectorProxyFactory {
  private MetricsCollector metricsCollector;
  public MetricsCollectorProxyFactory () {
    this.metricsCollector = new MetricsCollector();
  }
  public Object createProxy(Object proxiedObject) {
    Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
    DynamicProxyHandler handler = new DynamicProxyHandler(proxiedObject);
    return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, handler);
  }
  private class DynamicProxyHandler implements InvocationHandler {
    private Object proxiedObject;
    public DynamicProxyHandler(Object proxiedObject) {
      this.proxiedObject = proxiedObject;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      long startTimestamp = System.currentTimeMillis();
      Object result = method.invoke(proxiedObject, args);
      long endTimeStamp = System.currentTimeMillis();
      long responseTime = endTimeStamp - startTimestamp;
      String apiName = proxiedObject.getClass().getName() + ":" + method.getName();
      RequestInfo requestInfo = new RequestInfo(apiName, responseTime, startTimestamp);
      metricsCollector.recordRequest(requestInfo);
      return result;
    }
  }
}

客户端调用

//MetricsCollectorProxy使用举例,当需要为IUserController接口的UserController实现类进行性能计数器代理时,只需为UserController创建一个userControllerProxy代理类即可
MetricsCollectorProxyFactory proxyFactory = new MetricsCollectorProxyFactory ();
//通过MetricsCollectorProxyFactory 代理工厂创建一个IUserController业务的代理,包含性能计数器的实现
IUserController userControllerProxy = (IUserController) proxyFactory.createProxy(new UserController());
userControllerProxy.login("188****6234","tttttt");

模式扩展

上述内容只是代理模式的基本定义方式,也就是我们通常说的静态代理模式,动态代理模式更灵活,不违反开闭原则,实际软件工程中应用的很多都是动态代理,之前在学习各种框架时实际上已经简单探索过:

下图是动态代理的实现方式

总结一下

其实代理模式之前在项目中用到很多,包括开源框架原理学习(mybatis和spring aop)、远程proxy调用(retrofit2)等,只是当时对这些实际应用没有一个准确的归纳,现在学习完后才知道其实之前用到的各种代理其设计思想都是来源于这种设计模式。所以说设计模式其实是一种思想,掌握了思想就能更好的理解思想在实际场景中的各种应用,不变应万变。

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
13天前
|
设计模式 测试技术 Python
《手把手教你》系列基础篇(九十二)-java+ selenium自动化测试-框架设计基础-POM设计模式简介(详解教程)
【7月更文挑战第10天】Page Object Model (POM)是Selenium自动化测试中的设计模式,用于提高代码的可读性和维护性。POM将每个页面表示为一个类,封装元素定位和交互操作,使得测试脚本与页面元素分离。当页面元素改变时,只需更新对应页面类,减少了脚本的重复工作和维护复杂度,有利于团队协作。POM通过创建页面对象,管理页面元素集合,将业务逻辑与元素定位解耦合,增强了代码的复用性。示例展示了不使用POM时,脚本直接混杂了元素定位和业务逻辑,而POM则能解决这一问题。
29 6
|
11天前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十四)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-下篇(详解教程)
【7月更文挑战第12天】在本文中,作者宏哥介绍了如何在不使用PageFactory的情况下,用Java和Selenium实现Page Object Model (POM)。文章通过一个百度首页登录的实战例子来说明。首先,创建了一个名为`BaiduHomePage1`的页面对象类,其中包含了页面元素的定位和相关操作方法。接着,创建了测试类`TestWithPOM1`,在测试类中初始化WebDriver,设置驱动路径,最大化窗口,并调用页面对象类的方法进行登录操作。这样,测试脚本保持简洁,遵循了POM模式的高可读性和可维护性原则。
15 2
|
12天前
|
设计模式 Java 测试技术
《手把手教你》系列基础篇(九十三)-java+ selenium自动化测试-框架设计基础-POM设计模式实现-上篇(详解教程)
【7月更文挑战第11天】页面对象模型(POM)通过Page Factory在Java Selenium测试中被应用,简化了代码维护。在POM中,每个网页对应一个Page Class,其中包含页面元素和相关操作。对比之下,非POM实现直接在测试脚本中处理元素定位和交互,代码可读性和可维护性较低。
14 0
|
14天前
|
存储 设计模式 Java
Java面试题:解释代理模式的概念,并举例说明其应用场景。
Java面试题:解释代理模式的概念,并举例说明其应用场景。
9 0
|
14天前
|
设计模式 存储 缓存
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
Java面试题:结合设计模式与并发工具包实现高效缓存;多线程与内存管理优化实践;并发框架与设计模式在复杂系统中的应用
18 0
|
14天前
|
设计模式 缓存 安全
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
Java面试题:设计模式在并发编程中的创新应用,Java内存管理与多线程工具类的综合应用,Java并发工具包与并发框架的创新应用
13 0
|
14天前
|
设计模式 并行计算 安全
Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
Java面试题:如何使用设计模式优化多线程环境下的资源管理?Java内存模型与并发工具类的协同工作,描述ForkJoinPool的工作机制,并解释其在并行计算中的优势。如何根据任务特性调整线程池参数
16 0
|
14天前
|
设计模式 安全 Java
Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
Java面试题:请列举三种常用的设计模式,并分别给出在Java中的应用场景?请分析Java内存管理中的主要问题,并提出相应的优化策略?请简述Java多线程编程中的常见问题,并给出解决方案
24 0
|
设计模式 Java
【Java设计模式 设计模式与范式】结构型模式 一:适配器模式
【Java设计模式 设计模式与范式】结构型模式 一:适配器模式
51 0
|
设计模式 Java
【玩转23种Java设计模式】结构型模式篇:适配器模式
适配器模式(Adapter Pattern)将某个类的接口转换成客户端期望的另一个接口表示,主的目的是兼容性,让原本因接口不匹配不能一起工作的两个类可以协同工作。适配器模式属于结构型模式,主要分为三类:类适配器模式、对象适配器模式、接口适配器模式。
【玩转23种Java设计模式】结构型模式篇:适配器模式