十一、GoF之代理模式

news2024/11/19 1:47:56

1 对代理模式的理解

在这里插入图片描述
【在程序中,对象A和对象B无法直接交互时。】
【在程序中,功能需要增强时。】
【在程序中,目标需要被保护时】

业务场景:系统中有A、B、C三个模块,使用这些模块的前提是需要用户登录,也就是说在A模块中要编写判断登录的代码,B模块中也要编写,C模块中还要编写,这些判断登录的代码反复出现,显然代码没有得到复用,可以为A、B、C三个模块提供一个代理,在代理当中写一次登录判断即可。代理的逻辑是:请求来了之后,判断用户是否登录了,如果已经登录了,则执行对应的目标,如果没有登录则跳转到登录页面。【在程序中,目标不但受到保护,并且代码也得到了复用。】

代理模式是GoF23种设计模式之一。属于结构型设计模式。

代理模式的作用是:为其他对象提供一种代理以控制对这个对象的访问。在某些情况下,一个客户不想或者不能直接引用一个对象,此时可以通过一个称之为“代理”的第三者来实现间接引用。代理对象可以在客户端和目标对象之间起到中介的作用,并且可以通过代理对象去掉客户不应该看到的内容和服务或者添加客户需要的额外服务。 通过引入一个新的对象来实现对真实对象的操作或者将新的对象作为真实对象的一个替身,这种实现机制即为代理模式,通过引入代理对象来间接访问一个对象,这就是代理模式的模式动机。


代理模式中的角色:

  • 代理类(代理主题)
  • 目标类(真实主题)
  • 代理类和目标类的公共接口(抽象主题):客户端在使用代理类时就像在使用目标类,不被客户端所察觉,所以代理类和目标类要有共同的行为,也就是实现共同的接口。

代理模式的类图:
在这里插入图片描述


代理模式在代码实现上,包括两种形式:

  • 静态代理
  • 动态代理

2 静态代理

有一个接口和实现类:

package com.powernode.proxy.service;

/**
 * 订单业务接口
 * 代理对象和目标对象的公共接口
 */
public interface OrderService {
    /**
     * 生成订单
     */
    void generate();
    /**
     * 修改订单信息
     */
    void modify();
    /**
     * 查看订单详情
     */
    void detail();
}
package com.powernode.proxy.service.impl;

import com.powernode.proxy.service.OrderService;

/**
 * 目标对象
 */
public class OrderServiceImpl implements OrderService {

    @Override
    // 目标方法
    public void generate() {
        // 模拟生成订单的耗时
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已生成");
    }

    @Override
    public void modify() {
        // 模拟修改订单的耗时
        try {
            Thread.sleep(400);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单已修改");
    }

    @Override
    public void detail() {
        // 模拟查看订单的耗时
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("订单详情");
    }
}

项目经理提出一个新的需求:要统计所有业务接口中每一个业务方法的耗时。

  • 解决方案一:硬编码,在每一个业务接口中的每一个业务方法中直接添加统计耗时的程序。

    • 这种方案的缺点:
      • 缺点一:违背OCP开闭原则。
      • 缺点二:代码没有得到复用。(相同的代码写了很多遍。)
  • 解决方案二:编写业务类的子类,让子类继承业务类,对每个业务方法进行重写。

    • 缺点一:虽然解决了OCP开闭原则。但是这种方式会导致耦合度很高,因为采用了继承关系。继承关系是一种耦合度非常高的关系,不建议使用。
    • 缺点二:代码没有得到复用。(相同的代码写了很多遍。)
  • 解决方案三:代理模式。(静态代理)

    为OrderService接口提供一个代理类。

    package com.powernode.proxy.service.impl.static_proxy;
    
    import com.powernode.proxy.service.OrderService;
    
    /**
     * 代理对象(代理对象 和目标对象要具有相同的行为,就要实现公共接口)
     * 客户端在使用代理对象的时候就像在使用目标对象一样
     */
    public class OrderServiceProxy implements OrderService {
    
        // 将目标对象作为代理对象的一个属性.关联关系,比继承关系耦合度低
        // 这里写一个公共接口。因为公共接口耦合度低
        // 目标对象 目标对象一定实现了OrderService接口
        private OrderService target;
    
        // 创建代理对象的时候,传一个目标对象给代理对象
        public OrderServiceProxy(OrderService target) {
            this.target = target;
        }
    
        @Override
        // 代理方法
        public void generate() {
            // 增强
            long begin = System.currentTimeMillis();
            // 调用目标对象的目标方法
            target.generate();
            long end = System.currentTimeMillis();
            System.out.println("耗时"+(end - begin)+"毫秒");
        }
    
        @Override
        public void modify() {
            // 增强
            long begin = System.currentTimeMillis();
            // 调用目标对象的目标方法
            target.modify();
            long end = System.currentTimeMillis();
            System.out.println("耗时"+(end - begin)+"毫秒");
        }
    
        @Override
        public void detail() {
            // 增强
            long begin = System.currentTimeMillis();
            // 调用目标对象的目标方法
            target.detail();
            long end = System.currentTimeMillis();
            System.out.println("耗时"+(end - begin)+"毫秒");
        }
    }
    

    编写客户端程序:

    package com.powernode.proxy.client;
    
    import com.powernode.proxy.service.OrderService;
    import com.powernode.proxy.service.impl.OrderServiceImpl;
    import com.powernode.proxy.service.impl.static_proxy.OrderServiceProxy;
    
    public class Test {
        public static void main(String[] args) {
            // 创建目标对象
            OrderService target = new OrderServiceImpl();
            // 创建代理对象
            OrderService proxy = new OrderServiceProxy(target);
            // 调用代理对象的代理方法
            proxy.generate();
            proxy.modify();
            proxy.detail();
        }
    }
    

    以上就是代理模式中的静态代理,其中OrderService接口是代理类和目标类的共同接口。OrderServiceImpl是目标类。OrderServiceProxy是代理类。

    优点1:解决了OCP问题。
    优点2:采用代理模式的has a,可以降低耦合度。

目前我们使用的是静态代理,这个静态代理的缺点是什么?
        类爆炸。假设系统中有1000个接口,那么每个接口都需要对应代理类,这样类会急剧膨胀。不好维护。

怎么解决类爆炸问题?
        可以使用动态代理来解决这个问题。

动态代理还是代理模式,只不过添加了字节码生成技术,可以在内存中为我们动态的生成一个class字节码,这个字节码就是代理类。
在内存中动态的生成字节码代理类的技术,叫做:动态代理。

3 动态代理

在程序运行阶段,在内存中动态生成代理类,被称为动态代理,目的是为了减少代理类的数量。解决代码复用的问题。

在内存当中动态生成类的技术常见的包括:

  • JDK动态代理技术:只能代理接口。
  • CGLIB动态代理技术:CGLIB(Code Generation Library)是一个开源项目。是一个强大的,高性能,高质量的Code生成类库,它可以在运行期扩展Java类与实现Java接口。它既可以代理接口,又可以代理类,底层是通过继承的方式实现的。性能比JDK动态代理要好。(底层有一个小而快的字节码处理框架ASM。
  • Javassist动态代理技术:Javassist是一个开源的分析、编辑和创建Java字节码的类库。是由东京工业大学的数学和计算机科学系的 Shigeru Chiba (千叶 滋)所创建的。它已加入了开放源代码JBoss 应用服务器项目,通过使用Javassist对字节码操作为JBoss实现动态"AOP"框架。

3.1 JDK动态代理

在静态代理的时候,除了接口和实现类之外,还要写一个代理类OrderServiceProxy
在动态代理中OrderServiceProxy 代理类是可以动态生成的。这个类不需要写。我们直接写客户端程序即可:

package com.powernode.proxy.client;

import com.powernode.proxy.service.OrderService;
import com.powernode.proxy.service.impl.OrderServiceImpl;
import com.powernode.proxy.service.impl.TimerInvocationHandler;

import java.lang.reflect.Proxy;

public class DynamicProxyJDKTest {
    public static void main(String[] args) {
        // 创建目标对象
        OrderService target = new OrderServiceImpl();
        // 创建代理对象
        // 参数:类加载器,代理类要实现的接口,调用处理器
        OrderService proxyObj = (OrderService)Proxy.newProxyInstance(target.getClass().getClassLoader(),
                target.getClass().getInterfaces(),
                new TimerInvocationHandler(target));
        // 调用代理对象的代理方法
        proxyObj.generate();
        proxyObj.modify();
        proxyObj.detail();
    }
}

Proxy类全名:java.lang.reflect.Proxy。这是JDK提供的一个类(所以称为JDK动态代理)。主要是通过这个类在内存中生成代理类的字节码。

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

  1. newProxyInstance : 新建代理对象
    也就是说,通过调用这个方法可以创建代理对象。
    本质上,这个Proxy.newProxyInstance()方法的执行,做了两件事:
    第一件事:在内存中动态的生成了一个代理类的字节码class。
    第二件事:new对象了。通过内存中生成的代理类这个代码,实例化了代理对象。
  2. 关于newProxyInstance()方法的三个重要的参数,每一个什么含义,有什么用?
    1. 第一个参数:ClassLoader loader
      类加载器。这个类加载器有什么用呢?
      在内存当中生成的字节码也是class文件,要执行也得先加载到内存当中。加载类就需要类加载器。所以这里需要指定类加载器。
      并且JDK要求,目标类的类加载器必须和代理类的类加载器使用同一个。

    2. 第二个参数:Class<?>[] interfaces
      接口类型
      代理类和目标类要实现同一个接口或同一些接口。
      在内存中生成代理类的时候,这个代理类是需要你告诉它实现哪些接口的。

    3. 第三个参数:InvocationHandler h
      InvocationHandler 被翻译为:调用处理器。是一个接口。
      在调用处理器接口中编写的就是:增强代码。
      既然是接口,就要写接口的实现类。
      这是一个JDK动态代理规定的接口,接口全名:java.lang.reflect.InvocationHandler。显然这是一个回调接口,也就是说调用这个接口中方法的程序已经写好了,就差这个接口的实现类了。

java.lang.reflect.InvocationHandler接口的实现类,并且实现接口中的方法,代码如下:

package com.powernode.proxy.service.impl;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

/**
 * 专门负责计时的调用处理器对象
 * 在这个调用处理器当中编写计时相关的增强代码
 */
public class TimerInvocationHandler implements InvocationHandler {

    // 目标对象
    private Object target;

    // 通过构造方法来传目标对象
    // 赋值给成员变量
    public TimerInvocationHandler(Object target) {
        this.target = target;
    }
    
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 这个接口的目的就是写增强代码
        // 目标执行之前增强。
        long begin = System.currentTimeMillis();

        // 调用目标对象上的目标方法
        // 方法四要素:哪个对象,哪个方法,传什么参数,返回什么类型
        // invoke方法执行过程中,使用method来调用目标对象的目标方法。
        Object retValue = method.invoke(target, args);

        // 目标执行之后增强。
        long end = System.currentTimeMillis();
        System.out.println("耗时" + (end - begin) + "毫秒");

        // 注意这个invoke方法的返回值,如果代理对象调用代理方法之后,需要返回结果的话,invoke方法必须将目标对象的目标方法执行结果继续返回。
        return retValue;
    }
}
  1. invoke方法不是我们负责调用的,JDK负责调用

  2. invoke方法什么时候调用

    当代理对象调用代理方法的时候,注册在InvocationHandler调用处理器当中的invoke()方法被调用

  3. invoke方法的三个参数:

    invoke方法是JDK负责调用的,所以JDK调用这个方法的时候会自动给我们传过来这三个参数。

    我们可以在invoke方法的大括号中直接使用。

    第一个参数:Object proxy 代理对象的引用。设计这个参数只是为了后期的方便,如果想在invoke方法中使用代理对象的话,尽管通过这个参数来使用。

    第二个参数:Method method 目标对象上的目标方法。(要执行的目标方法就是它。)

    第三个参数:Object[] args 目标方法上的实参。

3.2 CGLIB动态代理

CGLIB既可以代理接口,又可以代理类。底层采用继承的方式实现。所以被代理的目标类不能使用final修饰。
使用CGLIB,需要引入依赖

<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>3.3.0</version>
</dependency>

准备一个没有实现接口的类

package com.dynamic.proxy.cglib.service;

/**
 * 目标类
 */
public class UserService {
    // 目标方法
    public boolean login(String username, String password){
        System.out.println("正在登录");
        if ("admin".equals(username) && "123".equals(password)) {
            return true;
        }
        return false;
    }

    // 目标方法
    public void logout(){
        System.out.println("退出系统");
    }
}

使用CGLIB在内存中为UserService类生成代理类,并创建对象:

package com.dynamic.proxy.cglib.client;

import com.dynamic.proxy.cglib.service.TimerMethodInterceptor;
import com.dynamic.proxy.cglib.service.UserService;
import net.sf.cglib.proxy.Enhancer;

public class Client {
    public static void main(String[] args) {
        // 创建字节码增强器对象
        // 这个对象是CGLIB库中的核心对象,依靠它来生成代理类
        Enhancer enhancer = new Enhancer();

        // 告诉CGLIB父类(目标类)
        enhancer.setSuperclass(UserService.class);

        // 设置回调接口(等同于JDK动态代理中的调用处理器,InvocationHandler)
        // 在CGLIB中是方法拦截器接口:MethodInterceptor
        enhancer.setCallback(new TimerMethodInterceptor());

        // 创建代理对象
        // 这一步做两件事
        // 1. 在内存中生成UserService类的子类(代理类的字节码)
        // 2. 创建代理对象 new
        // 生成源码,编译class,加载到JVM,并创建代理对象
        // 父类是UserService,子类一定也是UserService
        UserService userServiceProxy = (UserService) enhancer.create();

        // CGLIB动态代理生成的代理对象的名字格式
        // com.dynamic.proxy.cglib.service.UserService$$EnhancerByCGLIB$$65b0e71c@5d3411d
        System.out.println(userServiceProxy);

        // 调用代理对象的代理方法
        boolean success = userServiceProxy.login("admin", "123");
        System.out.println(success ? "登录成功" : "登录失败");
        userServiceProxy.logout();
    }
}

底层本质
class UserService$$EnhancerByCGLIB$$65b0e71c extends UserService{}

net.sf.cglib.proxy.MethodInterceptor
编写MethodInterceptor接口实现类:

package com.dynamic.proxy.cglib.service;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

public class TimerMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object target, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        // 前面增强
        long begin = System.currentTimeMillis();
        // 调用目标对象的目标方法
        Object retValue = methodProxy.invokeSuper(target, objects);

        // 后面增强
        long end = System.currentTimeMillis();
        System.out.println("耗时"+(end - begin)+"毫秒");

        // 返回方法的结果
        return retValue;
    }
}

MethodInterceptor接口中有一个方法intercept(),该方法有4个参数:

第一个参数:目标对象
第二个参数:目标方法
第三个参数:目标方法调用时的实参
第四个参数:代理方法

对于高版本的JDK,如果使用CGLIB,需要在启动项中添加两个启动参数:
在这里插入图片描述

  • –add-opens java.base/java.lang=ALL-UNNAMED
  • –add-opens java.base/sun.net.util=ALL-UNNAMED






JDK动态代理和CGLIB动态代理的区别?

1、JDK动态代理只能代理接口,CGLIB动态代理既可以代理接口,也可以代理类。
2、JDK动态代理的底层是采用实现接口的方式实现的,而CGLIB动态代理底层是使用继承实现的。
3、CGLIB动态代理的效率比JDK动态代理的高。

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

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

相关文章

HTML的表单标签

&#x1f31f;所属专栏&#xff1a;HTML只因变凤凰之路&#x1f414;作者简介&#xff1a;rchjr——五带信管菜只因一枚&#x1f62e;前言&#xff1a;该系列将持续更新HTML的相关学习笔记&#xff0c;欢迎和我一样的小白订阅&#xff0c;一起学习共同进步~&#x1f449;文章简…

将对象或数组存在 dom元素的属性上,最后取不到完整数据,只取到 [{

目录 一、问题 二、问题及解决方法 三、总结 一、问题 1.我需要在dom元素里面添加了一个属性test存一个对象数组temp&#xff0c;以便我下一次找到这个dom元素时可以直接拿到属性里面的数据来渲染页面。 2.dom 属性上存 对象和数组&#xff0c;必须先JSON.stringify(arr),转…

开发手册——一、编程规约_8.注释规约

这篇文章主要梳理了在java的实际开发过程中的编程规范问题。本篇文章主要借鉴于《阿里巴巴java开发手册终极版》 下面我们一起来看一下吧。 1. 【强制】类、类属性、类方法的注释必须使用 Javadoc 规范&#xff0c;使用 /**内容*/ 格式&#xff0c;不得使用 // xxx 方式。 说…

C++ 线程库

文章目录thread 创建mutexmutexrecursive_mutextimed_mutexlock_guard原子操作atomic条件变量condition_variable其他线程安全问题shared_ptr单例模式C 线程库是 C11 标准中引入的一个特性&#xff0c;它使得 C 在语言级别上支持多线程编程&#xff0c;不需要依赖第三方库或操作…

unity开发知识点小结01

unity对象生命周期函数 Awake():最早调用&#xff0c;所以可以实现单例模式 OnEnable&#xff08;&#xff09;&#xff1a;组件激活后调用&#xff0c;在Awake后调用一次 Stat&#xff08;&#xff09;&#xff1a;在Update&#xff08;&#xff09;之前&#xff0c;OnEnable…

【C++知识点】位运算

✍个人博客&#xff1a;https://blog.csdn.net/Newin2020?spm1011.2415.3001.5343 &#x1f4da;专栏地址&#xff1a;C/C知识点 &#x1f4e3;专栏定位&#xff1a;整理一下 C 相关的知识点&#xff0c;供大家学习参考~ ❤️如果有收获的话&#xff0c;欢迎点赞&#x1f44d;…

海思嵌入式开发-005-OpenHarmony源码编译问题

海思嵌入式开发-005-OpenHarmony源码编译问题一、问题描述二、解决方案2.1解决原理2.2获取OpenHarmony 3.1.1 Release源码2.3最后解决问题&#xff0c;编译成功。一、问题描述 按照链接拉取master源码&#xff0c;出现如下问题&#xff0c;打开build.log文件 提示相应位置的文…

Servlet详细教程

文章目录Servletservlet 简介Servlet 入门案例页面编写页面提交 get 请求Servlet 和 Tomcat 关系servlet-apiget 和 post 请求Servlet 生命周期案例HttpServletRequest 接口简介文件上传FileServlet 类Servlet servlet 简介 servlet 全称为 server applet 是服务器的小程序&am…

龙腾iSharedisk无盘系统 v1.8 Build 20230207 Crack

龙腾 iShareDisk无盘系统是一款高品质的 无盘启动和VHD离线启动系统。其功能满足目前校园、网咖、企业、酒店、证券、服务业、KTV、包厢VOD的需求&#xff0c;其可以 自行选择部署有盘或者无盘&#xff0c;实现Windows全系列产品无盘/VHD 启动的一体化解决方案&#xff01; …

【Storm】【七】Storm三种打包方式对比分析

Storm三种打包方式对比分析 一、简介二、mvn package三、maven-assembly-plugin插件四、maven-shade-plugin插件五、结论六、打包注意事项一、简介 在将 Storm Topology 提交到服务器集群运行时&#xff0c;需要先将项目进行打包。本文主要对比分析各种打包方式&#xff0c;并…

MyBatis - 14 - 分页插件的配置及使用

文章目录1、分页插件配置&#xff08;1&#xff09;在pom.xml中添加依赖&#xff08;2&#xff09;在MyBatis的核心配置文件中配置插件2、分页插件的使用回顾Mysql分页功能MyBatis分页插件的使用测试显示第1页&#xff0c;每页显示4条数据&#xff0c;打印page对象测试获取分页…

A. Linova and Kingdom(dfs + 贪心)

A. Linova and Kingdom&#xff08;dfs 贪心&#xff09;一、问题二、思路三、代码一、问题 二、思路 这道题的大意就是&#xff0c;给我们一棵树&#xff0c;我们需要在树上选择kkk个点&#xff0c;然后让kkk个信使从我们选取的kkk个点向第一个点出发。 我们把我们选取的k个…

Verdaccio 搭建私有 npm 仓库

背景 公司内部封装业务相关的组件库&#xff0c;工具库&#xff0c;希望统一管理和维护&#xff0c;在多个项目中都能使用&#xff0c;同时希望不公开&#xff0c;只在局域网中使用。所以&#xff0c;需要搭建私有 npm 仓库 Verdaccio verdaccio 是一个能够创建私有 registr…

vue:vue2与vue3的区别

一、背景 vue2是指的2.X vue3是指的3.0以及更新的版本&#xff08;3.2版本在script标签里可以写setup&#xff0c;极大的简化了开发&#xff09; 本文对比两者区别。 二、官网 生命周期选项 | Vue.js API 参考 | Vue.js Vue.js - 渐进式 JavaScript 框架 | Vue.js Vue.…

Redis学习【11】之分布式系统

文章目录一 数据分区算法1.1 顺序分区1.1.1 轮询分区算法1.1.2 时间片轮转分区算法1.1.3 数据块分区算法1.1.4 业务主题分区算法1.2 哈希分区1.2.1 节点取模分区算法1.2.2 一致性哈希分区算法1.2.3 虚拟槽分区算法二 分布式系统环境搭建与运行2.1 系统搭建2.1.1 系统架构2.1.2 …

物理层的概述(可以说是对王道计算机网络的笔记)

目录前言物理层概述基本概念数据通信基础知识数据通信相关术语三种通信方式两种传输方式码元&#xff0c;速率、波特、带宽**练习题**奈氏准则和香农定理奈氏准则&#xff08;奈奎斯特定理&#xff09;香浓定理结尾前言 本章内容讲述了物理层的概念,也是我上个星期上课的内容&…

现代检测技术-期末复习

文章目录差动结构的优点偏差/零位/微差法的应用偏差法测量零位法测量微差法测量格罗布斯准则&#xff08;作业题&#xff09;最小二乘法自相关/互相关算法的应用&#xff08;教材和课件案例&#xff09;自相关性分析互相关分析&#xff1a;电子计数器测频法&#xff08;作业题&…

第53章 短信验证服务和登录的前端定义实现

1 向src\router\index.js添加定义 { path: /LoginSms, name: 手机号登录, component: () > import(../views/LoginSmsView.vue) }, { path: /Users/Register, name: 用户注册, component: () > import(../views/Users/RegisterView.vue), }, 2 向src\common\http.api.js添…

Javascript借用原型对象继承父类型方法

借用原型对象继承父类型方法 目的: 儿子继承父类属性和方法&#xff0c;父类之后新增的方法不会被儿子继承。 前言&#xff1a; 先理解一个问题&#xff1a; Son.prototype Father.prototype; 这一操作相当于把Son的原型对象指向Father。 意味着Son的prototype的地址与Fa…

Vue基础学习 v-指令(2) 本地应用(记事本)

v-bind 设置元素的属性&#xff08;比如&#xff1a;src&#xff0c;title&#xff0c;class&#xff09; v-bind:属性名值 <div id"app"><img v-bind:src"imgSrc" alt"" v-bind:title"imgTitle"></div><scrip…