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

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
RDS AI 助手,专业版
简介: 【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)等,只是当时对这些实际应用没有一个准确的归纳,现在学习完后才知道其实之前用到的各种代理其设计思想都是来源于这种设计模式。所以说设计模式其实是一种思想,掌握了思想就能更好的理解思想在实际场景中的各种应用,不变应万变。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
本实验从Web应用上云这个最基本的、最普遍的需求出发,帮助IT从业者们通过“阿里云Web应用上云解决方案”,了解一个企业级Web应用上云的常见架构,了解如何构建一个高可用、可扩展的企业级应用架构。
MySQL数据库入门学习
本课程通过最流行的开源数据库MySQL带你了解数据库的世界。 &nbsp; 相关的阿里云产品:云数据库RDS MySQL 版 阿里云关系型数据库RDS(Relational Database Service)是一种稳定可靠、可弹性伸缩的在线数据库服务,提供容灾、备份、恢复、迁移等方面的全套解决方案,彻底解决数据库运维的烦恼。 了解产品详情:&nbsp;https://www.aliyun.com/product/rds/mysql&nbsp;
相关文章
|
4月前
|
设计模式 Java Spring
Java 设计模式之责任链模式:优雅处理请求的艺术
责任链模式通过构建处理者链,使请求沿链传递直至被处理,实现发送者与接收者的解耦。适用于审批流程、日志处理等多级处理场景,提升系统灵活性与可扩展性。
504 2
|
4月前
|
设计模式 网络协议 数据可视化
Java 设计模式之状态模式:让对象的行为随状态优雅变化
状态模式通过封装对象的状态,使行为随状态变化而改变。以订单为例,将待支付、已支付等状态独立成类,消除冗长条件判断,提升代码可维护性与扩展性,适用于状态多、转换复杂的场景。
449 0
|
6月前
|
设计模式 缓存 Java
Java设计模式(二):观察者模式与装饰器模式
本文深入讲解观察者模式与装饰器模式的核心概念及实现方式,涵盖从基础理论到实战应用的全面内容。观察者模式实现对象间松耦合通信,适用于事件通知机制;装饰器模式通过组合方式动态扩展对象功能,避免子类爆炸。文章通过Java示例展示两者在GUI、IO流、Web中间件等场景的应用,并提供常见陷阱与面试高频问题解析,助你写出灵活、可维护的代码。
|
4月前
|
设计模式 算法 搜索推荐
Java 设计模式之策略模式:灵活切换算法的艺术
策略模式通过封装不同算法并实现灵活切换,将算法与使用解耦。以支付为例,微信、支付宝等支付方式作为独立策略,购物车根据选择调用对应支付逻辑,提升代码可维护性与扩展性,避免冗长条件判断,符合开闭原则。
596 35
|
4月前
|
设计模式 消息中间件 传感器
Java 设计模式之观察者模式:构建松耦合的事件响应系统
观察者模式是Java中常用的行为型设计模式,用于构建松耦合的事件响应系统。当一个对象状态改变时,所有依赖它的观察者将自动收到通知并更新。该模式通过抽象耦合实现发布-订阅机制,广泛应用于GUI事件处理、消息通知、数据监控等场景,具有良好的可扩展性和维护性。
408 8
|
5月前
|
人工智能 Java API
Java与大模型集成实战:构建智能Java应用的新范式
随着大型语言模型(LLM)的API化,将其强大的自然语言处理能力集成到现有Java应用中已成为提升应用智能水平的关键路径。本文旨在为Java开发者提供一份实用的集成指南。我们将深入探讨如何使用Spring Boot 3框架,通过HTTP客户端与OpenAI GPT(或兼容API)进行高效、安全的交互。内容涵盖项目依赖配置、异步非阻塞的API调用、请求与响应的结构化处理、异常管理以及一些面向生产环境的最佳实践,并附带完整的代码示例,助您快速将AI能力融入Java生态。
844 12
|
6月前
|
设计模式 安全 Java
Java设计模式(一):单例模式与工厂模式
本文详解单例模式与工厂模式的核心实现及应用,涵盖饿汉式、懒汉式、双重检查锁、工厂方法、抽象工厂等设计模式,并结合数据库连接池与支付系统实战案例,助你掌握设计模式精髓,提升代码专业性与可维护性。
|
6月前
|
设计模式 XML 安全
Java枚举(Enum)与设计模式应用
Java枚举不仅是类型安全的常量,还具备面向对象能力,可添加属性与方法,实现接口。通过枚举能优雅实现单例、策略、状态等设计模式,具备线程安全、序列化安全等特性,是编写高效、安全代码的利器。
|
9月前
|
设计模式 Java 数据库连接
【设计模式】【创建型模式】工厂方法模式(Factory Methods)
一、入门 什么是工厂方法模式? 工厂方法模式(Factory Method Pattern)是一种创建型设计模式,它定义了一个用于创建对象的接口,但由子类决定实例化哪个类。工厂方法模式使类的实例化延迟
275 16
|
9月前
|
设计模式 负载均衡 监控
并发设计模式实战系列(2):领导者/追随者模式
🌟 ​大家好,我是摘星!​ 🌟今天为大家带来的是并发设计模式实战系列,第二章领导者/追随者(Leader/Followers)模式,废话不多说直接开始~
264 0