23种设计模式之代理模式

2022-09-26 15:10:39 浏览数 (1)

在有些情况下,一个客户不能或者不想直接访问另一个对象,这时需要找一个中介帮忙完成某项任务,这个中介就是代理对象。例如,购买火车票不一定要去火车站买,可以通过 12306 网站或者去火车票代售点买。又如找女朋友、找保姆、找工作等都可以通过找中介完成。

在软件设计中,使用代理模式的例子也很多,例如,要访问的远程对象比较大(如视频或大图像等),其下载要花很多时间。还有因为安全原因需要屏蔽客户端直接访问真实对象,如某单位的内部数据库等。

代理模式属于结构型模式,是23种设计模式中较为重要的一种,代理模式分为静态代理和动态代理,动态代理又分为基于接口实现的动态代理和基于子类实现的动态代理;在jdk源码中,很多底层的实现也是基于代理模式的,例如创建线程的方式之一实现Runnable接口就使用了静态代理模式,而Spring框架最为重要的AOP的实现是基于动态代理模式,可见,学习代理模式是我们能看懂底层源码实现原理的一个基础。

代理模式的定义与特点

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

代理模式的主要优点有:

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

其主要缺点是:

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

那么如何解决以上提到的缺点呢? 答案是可以使用动态代理方式

代理模式的结构与实现

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

模式的结构

代理模式的主要角色如下。

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

在代码中,一般代理会被理解为代码增强,实际上就是在原代码逻辑前后增加一些代码逻辑,而使调用者无感知。

根据代理的创建时期,代理模式分为静态代理和动态代理。

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

模式的实现

代理模式的实现代码如下:

代码语言:javascript复制
package proxy;
public class ProxyTest {
    public static void main(String[] args) {
        Proxy proxy = new Proxy();
        proxy.Request();
    }
}
//抽象主题
interface Subject {
    void Request();
}
//真实主题
class RealSubject implements Subject {
    public void Request() {
        System.out.println("访问真实主题方法...");
    }
}
//代理
class Proxy implements Subject {
    private RealSubject realSubject;
    public void Request() {
        if (realSubject == null) {
            realSubject = new RealSubject();
        }
        preRequest();
        realSubject.Request();
        postRequest();
    }
    public void preRequest() {
        System.out.println("访问真实主题之前的预处理。");
    }
    public void postRequest() {
        System.out.println("访问真实主题之后的后续处理。");
    }
}

程序运行的结果如下:

代码语言:javascript复制
访问真实主题之前的预处理。
访问真实主题方法...
访问真实主题之后的后续处理。

代理模式的应用场景

当无法或不想直接引用某个对象或访问某个对象存在困难时,可以通过代理对象来间接访问。使用代理模式主要有两个目的:一是保护目标对象,二是增强目标对象。

前面分析了代理模式的结构与特点,现在来分析以下的应用场景。

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

注意事项:

  • 1、和适配器模式的区别:适配器模式主要改变所考虑对象的接口,而代理模式不能改变所代理类的接口。
  • 2、和装饰器模式的区别:装饰器模式为了增强功能,而代理模式是为了加以控制。

代理模式的应用实例

  • 1、Windows 里面的快捷方式。
  • 2、猪八戒去找高翠兰结果是孙悟空变的,可以这样理解:把高翠兰的外貌抽象出来,高翠兰本人和孙悟空都实现了这个接口,猪八戒访问高翠兰的时候看不出来这个是孙悟空,所以说孙悟空是高翠兰代理类。
  • 3、买火车票不一定在火车站买,也可以去代售点。
  • 4、一张支票或银行存单是账户中资金的代理。支票在市场交易中用来代替现金,并提供对签发人账号上资金的控制。
  • 5、spring aop。

代理模式的扩展

在前面介绍的代理模式中,代理类中包含了对真实主题的引用,这种方式存在两个缺点。

  1. 真实主题与代理主题一一对应,增加真实主题也要增加代理。
  2. 设计代理以前真实主题必须事先存在,不太灵活。采用动态代理模式可以解决以上问题,如 SpringAOP

代理模式分类

静态代理

关于静态代理,我们以小明结婚为例,看以下代码:

代码语言:javascript复制
//代理对象和真实对象共同实现的接口,里面有结婚方法
interface A{
	void marry();
}

//真实对象
class Person implements A{
	@Override
	public void marry() {
		System.out.println("我要结婚啦~~~");
	}
}

//代理对象
class Proxy implements A{
    //代理类聚合了真实类
	private A a;
	public Proxy(A a){
		this.a = a;
	}
	@Override
	public void marry() {
		before();
		//真实对象去结婚
		a.marry();
		after();
	}
	//增强方法:对真实对象进行一个增强
	public void before(){
		System.out.println("结婚前,布置现场");
	}
	public void after(){
		System.out.println("结婚后,收拾现场");
	}
}

/**
 * 使用生活中的结婚例子来理解静态代理:小明要结婚,会去找婚庆公司帮忙准备结婚的一些事宜
 * 小明是真实对象,婚庆公司是代理对象,代理了真实对象,帮助小明去结婚,真正结婚的是小明
 * 
 * 静态代理模式通过代理对象可以做很多真实对象做不了的事情,实现对真实对象的一个增强
 * 同时真实对象也可以专注的去做一件事情
 * 
 * 就好比婚庆公司可以在小明结婚之前帮他布置现场,结婚之后收拾现场,而小明只需要专心结婚即可
 * 
 * 静态代理模式要求代理对象和真实对象实现同一个接口
 * @author cj_chen
 *
 */
public class Client {
	public static void main(String[] args) {
		//创建一个真实对象
		Person xiaoming = new Person();
		//创建一个代理对象,代理了小明这个真实对象
		Proxy proxy = new Proxy(xiaoming);
		//通过代理对象去执行真实对象的方法,实现一个增强
		proxy.marry();
	}
}

关于静态代理模式:

  1. 要求代理对象和真实对象实现同一个接口
  2. 一个真实角色就会产生一个代理角色,导致代码量翻倍,开发效率降低
  3. 静态代理模式通过代理对象可以做很多真实对象做不了的事情,实现对真实对象的一个增强,同时真实对象也可以专注的去做一件事情

静态代理模式在jdk源码中的应用:

实现Runnable接口创建线程的方式用到了静态代理模式;回顾一下步骤:先创建一个Runnable接口的实现类对象,然后创建一个Thread类,并将Runnable接口的实现类作为参数传递进去,然后调用Thread类对象的start方法开启多线程;

Runnable接口的实现类对象是真实对象,Thread类对象是代理对象,静态代理模式要求代理对象和真实对象继承同一个接口,Runnable接口的实现类对象和Thread类对象都继承了Runnable接口,所以完全符合静态代理模式

动态代理

关于动态代理,我们以生产者生产手机和销售手机为例,看以下代码:

代码语言:javascript复制
//被代理角色/真实角色实现的接口
public interface IProducer {
	//生产手机
	void produce();
	//销售手机
	void sale();
}

//被代理角色
public class Producer implements IProducer{
	@Override
	public void produce() {
		System.out.println("生产者生产手机啦~~~");
	}
	@Override
	public void sale() {
		System.out.println("生产者销售手机啦~~~");
	}
}

public class Client {
	public static void main(String[] args) {
		IProducer producer = new Producer();
		producer.produce();
	}
}

public class Client {
	public static void main(String[] args) {
		//先创建一个真实角色
		IProducer producer = new Producer();
		//基于接口方式创建动态代理角色
		IProducer proxy = (IProducer)Proxy.newProxyInstance(producer.getClass().getClassLoader(),
				producer.getClass().getInterfaces(),
				new InvocationHandler() {
					/**
		             * 作用:执行被代理对象的任何接口方法都会经过该方法
		             * 方法参数的含义
		             * @param proxy   代理对象的引用
		             * @param method  当前执行的方法
		             * @param args    当前执行方法所需的参数
		             * @return        和被代理对象方法有相同的返回值
		             * @throws Throwable
		             */
					@Override
					public Object invoke(Object proxy, Method method, Object[] args)
							throws Throwable {
						//增强真实角色的代码
						System.out.println("打印日志lalala~~~");
						return method.invoke(producer, args);
					}
				});
		proxy.produce();	
	}
}

一般来说,客户端可以直接调用真实对象的方法;现在我们有这么一个需求:想要在生产者(真实对象)执行生产方法和销售方法之前打印一句日志,并且是要在不修改原有代码的前提下完成,这也是为了满足设计模式的开闭原则,这时候就可以使用动态代理模式

关于动态代理:

  1. 分类
    1. 基于接口的动态代理:使用JDK官方给我们提供好的一个Proxy类(java.lang.reflect)来动态的创建代理对象,创建方法是使用Proxy类中的newProxyInstance方法,该方法需要传递三个参数,一是ClassLoader类加载器,用于加载代理对象字节码的,和被代理对象使用相同的类加载器;二是Class[]字节码数组,用于让代理对象和被代理对象有相同方法;三是InvocationHandler,用于提供增强的代码,也就是让我们写如何代理;基于接口的动态代理弊端就是要求被代理类最少实现一个接口,如果没有则不能使用
    2. 基于子类的动态代理:需要依赖第三方cglib库,如果是maven项目则导入cglib相关的依赖即可,基于子类的动态代理要求被代理类不能是最终类
  2. 好处
    1. 不修改原有代码的基础上对方法进行增强
    2. 解决了静态代理一个真实对象必须有一个代理对象,导致类数量大大增加的缺点,一个动态代理类可以代理多个类,只要实现的是同一个接口即可

动态代理模式底层使用的就是java的反射,要想真正理解动态代理模式,建议先看一下反射相关的知识!如果对于动态代理模式“一个动态代理类可以代理多个类,只要实现的是同一个接口即可”这个优点不够理解的话,

动态代理的应用:

Spring的AOP(面向切面编程)就运用到了动态代理模式,换言之,AOP的实现方式就是使用动态代理技术,AOP就是在不改变原有代码的基础上,可以将要增强的代码横切进去,例如要给原来的业务逻辑代码增加打印日志或者事务管理等;简单的说它就是把我们程序重复的代码抽取出来,在需要执行的时候,使用动态代理的技术,在不修改源码的 基础上,对我们的已有方法进行增强

号外!号外!

最近面试BAT,整理一份面试资料,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。想获取吗?如果你想提升自己,并且想和优秀的人一起进步,感兴趣的朋友,可以在扫码关注下方公众号。资料在公众号里静静的躺着呢。。。

  • 喜欢就收藏
  • 认同就点赞
  • 支持就关注
  • 疑问就评论

一键四连,你的offer也四连

————————————————————————————————————————————————————————————————

0 人点赞