静态代理还是动态代理?来聊聊Java中的代理设计模式

news2025/1/11 18:33:42

代理模式(Proxy Design Pattern)是一种结构型设计模式,为一个对象提供一个代理对象,然后使用代理对象控制对原对象的引用。即通过代理对象访问目标对象。被代理的对象可以是远程对象、创建开销大的对象或需要安全控制的对象。

一、代理模式介绍

代理模式主要有两个部分:

  • 抽象主题:声明一个公共接口,给代理类和真实对象进行实现。让真实对象和代理对象一一对应
  • 真实主题:定义所要代理的真实对象,其中包括实际的业务逻辑和操作
  • 代理主题:首先代理对象有真实对象的所有接口,保证能完全替代真实对象。此外还会有其他的方法和操作,来扩展相关的功能

其主要结构的UML图如下所示:

在这里插入图片描述

  1. Subject:抽象主题类,通过接口或抽象类声明主题和代理对象实现的业务方法
  2. RealSubject:真实主题类,实现Subject中的具体业务,是代理对象所代表的真实对象
  3. ProxySubject:代理类,其内部含有对真实主题的引用,它可以访问、控制或扩展RealSubject的功能
  4. Client:客户端,通过使用代理类来访问真实的主题类

按照上面的类图,可以实现如下代码:

//主题类接口
public interface Subject {
    void Request();
}

//真实的主题类
public class RealSubject implements Subject{

    @Override
    public void Request() {
        System.out.println("我是真实的主题类");
    }
}

//代理类
public class Proxy implements Subject{

    private RealSubject realSubject = new RealSubject(); //保证对真实对象的引用
	
    public void PreRequest(){
	 ....//调用Request()前的操作
    }
    
    @Override
    public void Request() {
        PreRequest();
        //对真实对象的调用
        realSubject.Request();
        postRequest();
    }
    
    public void PostRequest(){
        ....//调用Request()后的操作
    }
    
  
}

//客户端
public class Client {
    public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.Request();
    }
}

代理模式有比较广泛的使用,比如Spring AOPRPC、缓存等。在 Java 中,根据代理的创建时期,可以将代理模式分为静态代理和动态代理,下面就来分别阐述:

二、代理模式实现

动态代理和静态代理的区分就是语言类型是在运行时检查还是在编译期检查。大白话就是静态代理中的代理类是程序员自己写的,动态代理中的代理类程序员不用写,在代码运行过程中它能自动生成。

2.1 静态代理

静态代理是指在编译期,也就是在JVM运行之前就已经获取到了代理类的字节码信息。即Java源码生成.class文件时期:

在这里插入图片描述
由于在JVM运行前代理类和真实主题类已经是确定的,因此也被称为静态代理。

在实际使用中,通常需要定义一个公共接口及其方法,被代理对象(目标对象)与代理对象一起实现相同的接口或继承相同的父类。也就是我们自己需要构造一个代理类。在实际的日常开发中,几乎看不到使用静态代理的场景。

2.2 动态代理

动态代理,也就是在JVM运行时期动态构建对象和动态调用代理方法。它主要的作用就是可以帮助动态生成代理类,可以大大减少代理类的数量。

在JDK中,常用的实现方式是反射。而反射机制是指程序在运行期间可以访问、检测和修改其本身状态或行为的一种能力,使用反射我们可以调用任意一个类对象,以及其中包含的属性及方法。比如JDK Proxy。

此外动态代理也可以通过ASM(Java 字节码操作框架)来实现。比如CGLib。下面就具体进行说明:

2.2.1 JDK 动态代理

JDK 动态代理是JDK自身提供的一种方式,它的实现不需要引用第三方类,只需要实现InvocationHandler接口,重写invoke()方法就可以动态创建代理类。

  1. 实现InvocationHandler 接口,创建动态代理

这是减少代理类数量的核心部分,通过实现InvocationHandler 接口,并重写invoke() 方法,来创建动态代理类:

//核心部分 JDK Proxy 代理类
class JDKProxy implements InvocationHandler {
    //真实的委托对象
    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        //获得代理对象
        return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), this);
    }
    /**
	* @param proxy 代理对象,动态生成的代理类实例
	* @param method 被调用的方法对象,即要执行的方法,可以通过该对象获取方法的相关信息
	* @param args 方法的参数数组,这是被调用方法的参数列表。可以通过该参数获取方法的参数值
	* @return Object
	* @throws Throwable
	*/
    @Override   
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //执行委托对象的内部方法
        Object result = method.invoke(target, args);
        return result;
    }
}
  1. 创建被代理对象

也就是需要被代理的真实对象,这里也可以创建多个对象:

public interface Vehicle {
    void running();
}
public class Car implements Vehicle {
    @Override
    public void running() {
        System.out.println("Car is running");
    }
}
public class Bus implements Vehicle {
    @Override
    public void running() {
        System.out.println("Bus is running");
    }
}
public class Taxi implements Vehicle {
    @Override
    public void running() {
        System.out.println("Taxi is runnig");
    }
}
  1. 客户端调用实现

这里我们就能够通过一个代理类JDKProxy 来动态代理多个委托类了:

public class ProxyExampleDemo {
    public static void main(String[] args) {
        JDKProxy jdkProxy = new JDKProxy();
        Vehicle carInstance = (Vehicle) jdkProxy.getInstance(new Car());
        carInstance.running();
        Vehicle busInstance = (Vehicle) jdkProxy.getInstance(new Bus());
        busInstance.running();
        Vehicle taxiInstance = (Vehicle) jdkProxy.getInstance(new Taxi());
        taxiInstance.running();
    }
}
  1. 测试结果
Car is running
Bus is running
Taxi is running

从结果会发现通过一个JDKProxy,就代理实现多个真实对象的内部方法。但是存在一个问题,JDK提供的动态代理只能代理接口,而不能代理没有接口的类,有的,他就是Cglib。

2.2.2 CGLib 动态代理

CGLib类库可以代理没有接口的类,它采取的是创建目标类的子类的方式,通过子类化,我们可以达到近似使用被调用者本身的效果。实现代码如下所示:

  1. 核心代理类

和JDK proxy一样,需要实现一个接口MethodInterceptor ,但我们发现有些不同,其内部是通过实现被代理类的子类来实现动态代理的功能。所以在使用时一定要注意被代理的类不能使用final修饰

class CGLibProxy implements MethodInterceptor {
    private Object target;

    public Object getInstance(Object target) {
        this.target = target;
        Enhancer enhancer = new Enhancer();
        //设置父类为实例类
        enhancer.setSuperclass(this.target.getClass());
        //回调方法
        enhancer.setCallback(this);
        //创建代理对象
        return enhancer.create();
    }
    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        Object result = methodProxy.invokeSuper(o, objects);
        return result;
    }
}
  1. 创建被代理对象

这个被代理对象可以不用实现接口

class car {
    public void running() {
        System.out.println("car is running");
    }
}
  1. 客户端及测试
public class CGLibExample {
    public static void main(String[] args) {
        CGLibProxy cgLibProxy = new CGLibProxy();
        car instance = (car) cgLibProxy.getInstance(new car());
        instance.running();
    }
}
//测试结果:
// car is running
2.2.3 JDK Proxy 和 CGLib 的区别
  1. 来源:JDK Proxy 是JDK 自带的功能,CGLib 是第三方提供的工具
  2. 实现:JDK Proxy 通过拦截器加反射的方式实现;CGLib 基于ASM实现,性能比较高
  3. 接口:JDK Proxy 只能代理继承接口的类,CGLib 不需要通过接口来实现,它是通过实现子类的方式来完成调用,但是要注意被代理的类不能被final修饰

三、代理模式的应用场景

代理模式其实在日常的开发中并不常见,但是在一些框架中使用的很多,比如springAop, myBatis mapper, 远程代理等等

3.1 MapperProxyFactory

在MyBatis 中,也存在着代理模式的使用,比如MapperProxyFactory。其中的newInstance()方法就是生成一个具体的代理来实现功能,代码如下:

public class MapperProxyFactory<T> {
  private final Class<T> mapperInterface;
    
  private final Map<Method, MapperMethodInvoker> methodCache = new ConcurrentHashMap<>();
    
  public MapperProxyFactory(Class<T> mapperInterface) {
    this.mapperInterface = mapperInterface;
  }

  public Class<T> getMapperInterface() {
    return mapperInterface;
  }

  public Map<Method, MapperMethodInvoker> getMethodCache() {
    return methodCache;
  }

  // 创建代理类
  @SuppressWarnings("unchecked")
  protected T newInstance(MapperProxy<T> mapperProxy) {
    return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy);
  }

  public T newInstance(SqlSession sqlSession) {
    final MapperProxy<T> mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache);
    return newInstance(mapperProxy);
  }
}

3.2 Spring AOP

代理模式最常使用的一个应用场景就是在业务系统中开发一些非功能性需求,比如监控、统计、鉴权、限流、事务、日志等。将这些附加功能与业务功能解耦,放在代理类中统一处理,让程序员只需要关注业务方面的开发。而Spring AOP 的切面实现原理就是基于动态代理

在这里插入图片描述

Spring AOP 的底层通过上面提到的 JDK Proxy 和 CGLib动态代理机制,为目标对象执行横向织入。当Bean实现了接口时, Spring就会使用JDK Proxy,在没有实现接口时就会使用 CGLib。也可以在配置中强制使用 CGLib:

<aop:aspectj-autoproxy proxy-target-class="true"/>

3.3 远程代理

java 中的RMI(Remote Method Invocation),比如

RPC 框架的实现可以看作成是一种代理模式,通过远程代理、将网络同步、数据编解码等细节隐藏起来,让客户端在使用 RPC 服务时,不必考虑这些细节。

四、代理模式实战

4.1 利用静态代理模式实现模拟访问网页功能

这里可以使用静态代理模式来实现模拟访问网页的功能

  1. 定义访问网站的接口和统一访问方法
public interface PageVisitor {
    void visitPage();
}
  1. 实现真实访问和代理访问具体类

两个具体的访问类都要实现统一的访问接口

public class RealPageVisitor implements PageVisitor {

    private final OkHttpClient httpClient;

    public RealPageVisitor() {
        this.httpClient = new OkHttpClient();
    }

    @Override
    public void visitPage() {
        String url = "要访问的网站地址Url";
        Request request = new Request.Builder()
                .url(url)
                .build();
        try {
            Response response = httpClient.newCall(request).execute();
            if (response.isSuccessful()) {
                System.out.println("正在访问网址:" + url);
            } else {
                System.out.println("访问失败网址是:" + url);
            }
        } catch (IOException e) {
            System.out.println("出现访问异常信息");
            e.printStackTrace();
        }
    }
}

可以通过对代理类,对真实委托类进行扩展和管理:

public class ProxyPageVisitor implements PageVisitor {

    @Autowired
    private RealPageVisitor realPageVisitor;

    private int visitCount;

    public ProxyPageVisitor(RealPageVisitor realPageVisitor) {
        this.realPageVisitor = realPageVisitor;
        this.visitCount = 0;
    }

    @Override
    public void visitPage() {
        //管理真实代理的生命周期
        if (canVisit()) {
            realPageVisitor.visitPage();
            visitCount++;
        } else {
            throw new IllegalStateException("超出最大访问量");
        }

    }

    private boolean canVisit() {
        int maxVisitCount = 120;
        return visitCount < maxVisitCount;
    }

}
  1. 设置访问的定时任务

该部分也就相当于UML图中的client 客户端,调用代理类来实现具体业务逻辑

public class PageVisitorScheduler {

    @Autowired
    private ProxyPageVisitor pageVisitor;

    @Scheduled(fixedDelay = 25000)
    public void visitPage() {
        pageVisitor.visitPage();
    }
}

五、参考资料

http://c.biancheng.net/view/1359.html

https://kaiwu.lagou.com/course/courseInfo.htm?courseId=59#/detail/pc?id=1768

https://time.geekbang.org/column/article/7489

https://time.geekbang.org/column/article/201823

《Java 重学设计模式》

《大话设计模式》

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

JavaWeb——新闻管理系统(Jsp+Servlet)之jsp新闻查询

java-ee项目结构设计 1.dao:对数据库的访问&#xff0c;实现了增删改查 2.entity:定义了新闻、评论、用户三个实体&#xff0c;并设置对应实体的属性 3.filter&#xff1a;过滤器&#xff0c;设置字符编码都为utf8&#xff0c;防止乱码出现 4.service:业务逻辑处理 5.servlet:处…

Spring AI和Ollama

概述 Spring AI 不仅提供了与 OpenAI 进行API交互&#xff0c;同样支持与 Ollama 进行API交互。Ollama 是一个发布在GitHub上的项目&#xff0c;专为运行、创建和分享大型语言模型而设计&#xff0c;可以轻松地在本地启动和运行大型语言模型。 Docker环境安装Ollama 1.获取D…

第13课 利用openCV检测物体是否运动了

FFmpeg与openCV绝对是绝配。前面我们已经基本熟悉了FFmpeg的工作流程&#xff0c;这一章我们重点来看看openCV。 在前面&#xff0c;我们已经使用openCV打开过摄像头并在MFC中显示图像&#xff0c;但openCV能做的要远超你的想像&#xff0c;比如可以用它来实现人脸检测、车牌识…

个人笔记:分布式大数据技术原理(一)Hadoop 框架

Apache Hadoop 软件库是一个框架&#xff0c;它允许使用简单的编程模型&#xff0c;实现跨计算机集群的大型数据集的分布式处理。它最初的设计目的是为了检测和处理应用程序层的故障&#xff0c;从单个机器扩展到数千台机器&#xff08;这些机器可以是廉价的&#xff09;&#…

算法通关村第二十关-黄金挑战图的常见算法

大家好我是苏麟 , 今天聊聊图的常见算法 . 图里的算法是很多的&#xff0c;这里我们介绍一些常见的图算法。这些算法一般都比较复杂&#xff0c;我们这里介绍这些算法的基本含义&#xff0c;适合面试的时候装*&#xff0c;如果手写&#xff0c;那就不用啦。 图分析算法&#xf…

Qt/QML编程学习之心得:Timer的使用(22)

Qt中timer计时器如何使用? Timer的创建: void InitTimer(){myTimer = new QTimer(q);myTimer->setInterval(100); // 100msmyTimer->setSingleShot(true); //只运行一次的计时器QObject::connect(myTimer,SIGNAL(timeout()),q,SLOT(onTimeOut()));myTimer->start(…

(2023|NIPS,邻域分布预测,Wasserstein 距离)通过上下文预测改进基于扩散的图像合成

Improving Diffusion-Based Image Synthesis with Context Prediction 公和众和号&#xff1a;EDPJ&#xff08;添加 VX&#xff1a;CV_EDPJ 或直接进 Q 交流群&#xff1a;922230617 获取资料&#xff09; 目录 0. 摘要 3. 基础 4. ConPreDiff 4.1 扩散生成中的邻域上下…

Linux入门攻坚——11、Linux网络属性配置相关知识1

网络基础知识&#xff1a; 局域网&#xff1a;以太网&#xff0c;令牌环网&#xff0c; Ethernet&#xff1a;CSMA/CD 冲突域 广播域 MAC&#xff1a;Media Access Control&#xff0c;共48bit&#xff0c;前24bit需要机构分配&#xff0c;后24bit自己…

安装阿里云CLI之配置阿里云凭证信息

有时候需要再主机上通过 OpenAPI 的调用访问阿里云&#xff0c;并完成控制&#xff0c;此时就需要在服务器上安装阿里云CLI&#xff0c;并完成账号的设置。 1. 登录阿里云创建账号 1.1 点击阿里云头像 ——》 控制访问 ——》创建一个拥有DNS权限的用户 这个用户不用太多权限…

Service Weaver:Google开源基于分布式应用程序开发的框架,重新定义微服务边界

大家好&#xff0c;我是萧楚河&#xff0c;公众号&#xff1a;golang面试经典讲解&#xff0c;感谢关注&#xff0c;一起学习一起成长。一、前言 今年6月&#xff0c;一群谷歌员工&#xff08;由谷歌软件工程师Michael Whittaker领导&#xff09;发表了一篇名为“Towards Mode…

对接第三方接口鉴权(Spring Boot+Aop+注解实现Api接口签名验证)

前言 一个web系统&#xff0c;从接口的使用范围也可以分为对内和对外两种&#xff0c;对内的接口主要限于一些我们内部系统的调用&#xff0c;多是通过内网进行调用&#xff0c;往往不用考虑太复杂的鉴权操作。但是&#xff0c;对于对外的接口&#xff0c;我们就不得不重视这个…

MySQL--基础篇

这里写目录标题 总览MySQl各个阶段基础篇总览 MySQL概述数据库相关概念查看本机MySQL版本号启停mysql打开windows服务管理windows命令行启停 连接mysql客户端mysql运行逻辑数据模型关系型数据库 总结 SQL总览SQL通用语法SQL语句分类DDL数据库操作表操作查询表创建表结构数据类型…

有什么安全处理方案可以有效防护恶意爬虫

常见的爬虫 有百度爬虫、谷歌爬虫、必应爬虫等搜索引擎类爬虫&#xff0c;此类爬虫经常被企业用于提高站点在搜索引擎内的自然排名&#xff0c;使得站点在各大搜索引擎中的排名能够提高&#xff0c;进一步通过搜索引擎来进行引流为企业增加业务流量。 恶意爬虫与合法、合规的搜…

看了致远OA的表单设计后的思考

更多ruoyi-nbcio功能请看演示系统 gitee源代码地址 前后端代码&#xff1a; https://gitee.com/nbacheng/ruoyi-nbcio 演示地址&#xff1a;RuoYi-Nbcio后台管理系统 更多nbcio-boot功能请看演示系统 gitee源代码地址 后端代码&#xff1a; https://gitee.com/nbacheng/n…

微信小程序的驾校预约管理系统

&#x1f345;点赞收藏关注 → 私信领取本源代码、数据库&#x1f345; 本人在Java毕业设计领域有多年的经验&#xff0c;陆续会更新更多优质的Java实战项目希望你能有所收获&#xff0c;少走一些弯路。&#x1f345;关注我不迷路&#x1f345;一 、设计说明 1.1课题背景 在I…

【教学类-43-16】 20240106 推算5-9宫格数独可能出现的不重复题量(N宫格数独模板数量的推算)

作品展示&#xff1a; 通过对各种已有结果的人工推算&#xff0c;目前得到两个结论 一、阶乘基本样式的数量【【123】【321】【231】【132】【312】【312】】6组 结论&#xff1a;阶乘等于出现的基本样式数量 以下N*N格会出现的最大排序数量&#xff08;比如包含333222111这种…

Spring声明式事务业务bug

Spring 针对 Java Transaction API (JTA)、JDBC、Hibernate 和 Java Persistence API (JPA) 等事务 API&#xff0c;实现了一致的编程模型&#xff0c;而 Spring 的声明式事务功能更是提供了极其方便的事务配置方式&#xff0c;配合 Spring Boot 的自动配置&#xff0c;大多数 …

C++ 二进制图片的读取和blob插入mysql_stmt_init—新年第一课

关于二进制图片的读取和BLOB插入一共包含五步 第一步&#xff1a;初始化 MYSQL_STMT* stmt mysql_stmt_init(&mysql); 第二步&#xff1a;预处理sql语句 mysql_stmt_prepare(stmt,sql,sqllen); 第三步&#xff1a;绑定字段 mysql_stmt_bind_param(stmt,bind); 第四…

用友U8+CRM 逻辑漏洞登录后台漏洞复现

0x01 产品简介 用友U8 CRM客户关系管理系统是一款专业的企业级CRM软件&#xff0c;旨在帮助企业高效管理客户关系、提升销售业绩和提供优质的客户服务。 0x02 漏洞概述 用友 U8 CRM客户关系管理系统 reservationcomplete.php文件存在逻辑漏洞&#xff0c;未授权的攻击者通过…

手把手带你门SpringCloud

目录​​​​​ 1、什么是 SpringCloud&#xff1f; SpringCloud常用组件&#xff1a; 简单介绍组件间作用 2&#xff0c;SpringCloud相关组件&#xff1a;Eureka 3&#xff0c;Spring Cloud核心组件&#xff1a;Feign 4&#xff0c;Spring Cloud核心组件&#xff1a;Zuu…