[设计模式] - 代理模式(静态代理与动态代理)

一、代理模式简介

1. 什么是代理模式

代理模式(Proxy Pattern) 也叫做委托模式,属于结构性设计模式的一种,在设计模式之禅一书中其定义为: provide a surrogate or placeholder for another object to control access to it(为其他对象提供一种代理以控制对这个对象的访问)。简单的来讲其实我们的目的就是为一个业务类提供一个代理对象来完成业务逻辑,如果同学还是有疑问的话,那就请继续往下看。

2. 简单举例

不知道屏幕前的小伙伴平时会不会通过打游戏来消灭无聊的时间。博主的同事就有很多沉迷在了一款叫做王者荣耀的手游中。这类MMORPG有一种叫做排位的天梯机制,通过进行游戏来将自己的段位增加或减少。既然是游戏就肯定有输有赢,像博主这种菜鸡就属于又菜又爱玩的一类:
在这里插入图片描述
一天下来博主的段位就越打越低,这肯定不行呀,但是机智如博主就想到了一个很简单的办法:找代练。这里简单的解释下什么是找代练:代练就是指将自己的游戏账号交给游戏水平更高的玩家来进行操作来带到获利的目的。这时候我们不难发现,虽然进行游戏的还是我的账号,但是操作游戏的却已经换成了我的代练,这样就可以实现达到指定段位的目的。于是在代练不断地努力下,在赛季结束前,我终于达到了我想要的段位。

在这里插入图片描述
那么我们现在要做的就是分析一下代练的这个业务场景:
在我找代练帮我进行游戏之前,我打游戏的流程如下:
在这里插入图片描述

代码实现没有代练的业务场景:

  1. 创建一个游戏业务接口
public interface Player {

	void login(String username, String password);

	void play();

	void result();
}

  1. 创建游戏玩家
public class GamePlayer implements Player {

	private String username;
	private String password;

	@Override
	public void login(String username, String password) {
		this.username = username;
		this.password = password;
		System.out.println("用户:" + username + "已上线");
	}

	@Override
	public void play() {
		System.out.println("用户:" + username + "游戏开始");

	}

	@Override
	public void result() {
		System.out.println("用户:" + username + "游戏结束,获得胜利");

	}

}
  1. 创建客户端
public class Client {

	public static void main(String[] args) {
		GamePlayer gamePlayer = new GamePlayer();
		gamePlayer.login("张三", "123456");
		gamePlayer.play();
		gamePlayer.result();
	}
}

测试结果:
在这里插入图片描述

而在我找代练帮我进行游戏之后的流程就变成了:
在这里插入图片描述
代码实现:

  1. 游戏业务接口和用户类代码不变
  2. 新建代练对象
public class ProxyGamePlayer implements Player {

	private GamePlayer gamePlayer;

	@Override
	public void login(String username, String password) {
		// TODO Auto-generated method stub
		gamePlayer.login(username, password);

	}

	@Override
	public void play() {
		// TODO Auto-generated method stub
		gamePlayer.play();
		System.out.println("代练已经开始工作");
	}

	@Override
	public void result() {
		gamePlayer.result();
		System.out.println("代练工作结束");
	}

	public ProxyGamePlayer(GamePlayer gamePlayer) {
		super();
		this.gamePlayer = gamePlayer;
	}

}

  1. 测试代码
public class Client {

	public static void main(String[] args) {
		ProxyGamePlayer proxyGamePlayer = new ProxyGamePlayer(new GamePlayer());
		proxyGamePlayer.login("张三", "123456");
		proxyGamePlayer.play();
		proxyGamePlayer.result();
	}
}

测试结果:
在这里插入图片描述

其实到这里一个简单的代理模式就完成了,不难发现在这段代码中游戏的整体业务流程并没有发生什么变化,但是游戏的进行者变成了我的代练,而我无需在关心整个游戏流程,这就是代理模式的最典型应用:通过一个代理对象来完成被代理对象的业务逻辑以实现对整个业务流程的增强和监控。类似的场景还有我们平时租房时去寻找中介,打官司时去找律师,这类场景都有一个共同点就是我们将原本需要自己去做的事情未获给代理人,通过代理人来完成原本需要我们去做的事情,而我们作为委托人就不需要关心业务流程如何实现,那么接下来我们再详细的讲述下代理模式及其实现原理。

二、代理模式的设计思路

1. 代理模式的构成

代理模式主要是由三要素构成:

  1. 共同的行为(常用作接口或抽象类)
  2. 委托对象(真实对象,向代理对象提供委托)
  3. 代理对象(代理对象,负责实现真实对象的委托)

根据三要素,我们可以画出一个通用的类图:

在这里插入图片描述
代理模式的实现遵循着两个原则:

  1. 委托类与代理类又共同行为或相似的行为
  2. 代理类负责针对委托类进行增强和管理

但是在代理模式的实现形式上又分为了两种:

  1. 静态代理
  2. 动态代理

那么什么是静态代理,什么又是动态代理呢?两者之间有什么不同呢?

1. 静态代理

静态代理(static proxy) 是指程序的设计者在程序运行前就已经为委托对象创建好了代理对象,换句话说就是我们在启动服务之前,代理对象的.class文件就已经生成了。刚刚我们举的游戏代练例子就是最典型的静态代理形式,我们的代练对象ProxyGamePlayer在我们程序运行之前就已经被我通过编码的方式写死了代理策略,在程序运行后通过静态的方式提供代理服务。为了加深大家对静态代理的理解,我们再通过一个中介租房的例子来讲述一下静态代理模式。

有些小伙伴可能跟博主一样有过租房的经历,正常的租房流程可能是:
决定租房需求 -> 寻找房源 -> 相约看房 -> 签约 -> 入住
但是由于博主平时还要工作和学习,精力有限,可能无法提供充裕的时间来投入到寻找房源和约房东看房的流程中,这个时候我们可能就需要一名中介来帮我们完成原本应该我们去做的事情。当中介接入后,我们租房的流程并没有发生太多的变化,只不过是中介代替我去做了这些事情,而我只需要在流程结束后为中介结算工资即可。

下面我们用静态代理的形式来做一个简单的实现:
1. 首先定义一个租房的行为流程

public interface Renting {
	// 租房需求
	public void demand();

	// 寻找房源
	public void find();

	// 看房
	public void look();

	// 签约
	public void sign();

}

2. 创建委托对象

public class Tenant implements Renting {

	@Override
	public void demand() {
		System.out.println("需求是寻找一个靠近地铁站的三居室");

	}

	@Override
	public void find() {
		System.out.println("通过租房APP查找");

	}

	@Override
	public void look() {
		System.out.println("上门看房");

	}

	@Override
	public void sign() {
		System.out.println("与房东签约");

	}

}

3. 创建代理对象

public class RentingProxy implements Renting {

	private Renting renting;

	// 通过有参构造注入委托对象
	public RentingProxy(Renting renting) {
		super();
		this.renting = renting;
	}

	@Override
	public void demand() {
		renting.demand();
		System.out.println("中介已接收到客户需求");

	}

	@Override
	public void find() {
		renting.find();
		System.out.println("中介同时利用现有资源匹配客户需求");

	}

	@Override
	public void look() {
		renting.look();
		System.out.println("中介提供了上门录制视频代看服务");

	}

	@Override
	public void sign() {
		renting.sign();
		System.out.println("签约结束后向客户收取佣金");

	}

}

创建客户端

public class Client {

	public static void main(String[] args) {
		RentingProxy rentingProxy = new RentingProxy(new Tenant());
		rentingProxy.demand();
		rentingProxy.find();
		rentingProxy.look();
		rentingProxy.sign();
	}
}

测试结果:
在这里插入图片描述
在这里我们的RentingProxy对象在我们业务端运行之前就已经被我规定好了代理行为及写好相关代码,由于我们在客户端调用时传入了委托对象,因此如果我们有100个委托对象就需要创建100个代理对象,这就是静态代理。
那么静态代理都有哪些优缺点呢?
优点: 代理对象的代理行为更加纯粹,不需要关心一些公共的委托对象的属性行为,只需要专心服务自身的委托对象即可。
缺点: 代理对象必须提前创建好,如果委托对象发生变化,代理对象相应的也需要改变,维护成本高。

那么有没有一种方式可以针对静态代理不够灵活的特点做出优化呢?

2. 动态代理

动态代理(Dynamic Proxy):动态代理是java提供的一种运行时加载代理技术,相比于静态代理,动态代理在为对象提供代理服务的时候更加灵活,动态代理类可以在程序运行时由java通过反射动态的生成。简单的来说动态代理是在实现阶段不用关系我去代理谁,而在运行阶段才会去指定委托对象。常见的动态代理有两种实现形式:接口代理cglib代理

(1)接口代理

接口代理方式是java自带的一种动态代理模式,其通过Proxy类的静态方法newProxyInstance返回一个接口的代理实例已达到针对不同的委托类都能生成对应的代理类的目的。那么将接口代理之前讲清楚newProxyInstance是很有必要的,我们先看一下这个方法在调用的时候都需要哪些参数:
在这里插入图片描述
我们可以看到再到用这个方法的时候需要三个要素:

  1. 委派对象自身的classloader(类加载器)。
  2. 委派对象自身实现的接口
  3. 一个实现了invocationHandler接口的代理对象

OK,我们还是沿用中介租房的案例来做一个简单的动态代理实现。
1. 定义一个租房流程接口

public interface Renting {
	// 租房需求
	public void demand();
	// 寻找房源
	public void find();
	// 看房
	public void look();
	// 签约
	public void sign();
}

2. 创建一个接口的实现类

public class Tenant implements Renting {

	@Override
	public void demand() {
		System.out.println("需求是寻找一个靠近地铁站的三居室");
	}
	@Override
	public void find() {
		System.out.println("通过租房APP查找");
	}
	@Override
	public void look() {
		System.out.println("上门看房");
	}
	@Override
	public void sign() {
		System.out.println("与房东签约");
	}
}

3. 创建一个实现了invocationHandler的代理类

public class GamePlayerProxyHandler implements InvocationHandler {
	// 被代理实例
	private Object obj;
	public GamePlayerProxyHandler(Object obj) {
		super();
		this.obj = obj;
	}
	@Override
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object result = method.invoke(obj, args);
		System.out.println("安家中介公司为你服务");
		return result;
	}

}

4. 通过Proxy对象实现动态代理

public class Client {

	public static void main(String[] args) {
		Renting tenant = new Tenant();
		GamePlayerProxyHandler gamePlayerProxyHandler = new GamePlayerProxyHandler(tenant);
		// 获取委托对象的加载器
		ClassLoader classLoader = tenant.getClass().getClassLoader();
		Renting newProxyInstance = (Renting) Proxy.newProxyInstance(classLoader, new Class[] { Renting.class },
				gamePlayerProxyHandler);
		newProxyInstance.demand();
	}
}

测试结果:
在这里插入图片描述
如果感到疑惑地同学可以看下这段代码的简单结构图;
在这里插入图片描述
最后我们来总结下接口形式的动态代理有哪些特点呢?
优点: 相较于静态代理,动态代理极大程度的减少了开发工作量,同时减少了代理类对于业务接口的依赖,降低了耦合度。
缺点: 有些遗憾的是接口形式的动态代理还是有一点瑕疵,由于newProxyInstance方法中必须传入一个委托对象的接口,所以决定了如果想要用接口形式的动态代理,我们必须为委托对象创建一个Interface并实现它。
那么如果我们不想要为委托对象创建接口,该怎么办呢?

(2)Cglib代理

Cglib代理模式采用了非常底层的字节码技术来实现动态代理。其不要求委托类实现某一个接口,而是通过字节码技术为委托类创建一个子类,并通过拦截父类的方法调用的形式来动态的将代理逻辑织入到方法中,但是由于采用的是创建子类集成的形式,父类不可以用final来进行类修饰。这里需要注意的是cglib并不是java自带的API,因此我们想要使用前需要导入相关的依赖JAR包。

cglib项目的git地址:https://github.com/cglib/cglib

cglib的pom依赖:

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

如果还不会使用maven的小伙伴,也可以去我的网盘下载:

链接:https://pan.baidu.com/s/1Fs3ptOUTxOfdAErGR5ET0A
提取码:ck14

那么接下来我们还是通过中介租房的案例继续讲解下cglib的代码实现:
1. 这里我们将原本的租房流程接口转变为一个抽象类,用来讲述清楚两种动态代理的区别

public abstract class Renting {
	// 租房需求
	abstract void demand();

	// 寻找房源
	abstract void find();

	// 看房
	abstract void look();

	// 签约
	abstract void sign();
}

2. 创建委托类

public class Tenant extends Renting {

	@Override
	public void demand() {
		System.out.println("需求是寻找一个靠近地铁站的三居室");

	}

	@Override
	public void find() {
		System.out.println("通过租房APP查找");

	}

	@Override
	public void look() {
		System.out.println("上门看房");

	}

	@Override
	public void sign() {
		System.out.println("与房东签约");

	}

}

3. 创建cglib代理对象

public class CglibProxy implements MethodInterceptor {

	private Object target;

	public Object getProxyInstance() {
		Enhancer enhancer = new Enhancer();
		enhancer.setSuperclass(target.getClass());
		enhancer.setCallback(this);
		return enhancer.create();
	}

	public CglibProxy(Object target) {
		super();
		this.target = target;
	}

	@Override
	public Object intercept(Object target, Method method, Object[] param, MethodProxy proxy) throws Throwable {
		System.out.println("cglib代理开始");
		Object result = proxy.invokeSuper(target, param);
		System.out.println("cglib代理结束");
		return result;
	}

}

4. 测试代码(如果你不是一个maven工程,这里会报错,记得继续往下看)

public class Main {

	public static void main(String[] args) {
		CglibProxy cglibProxy = new CglibProxy(new Tenant());
		Tenant proxyInstance = (Tenant) cglibProxy.getProxyInstance();
		proxyInstance.demand();
	}
}

测试结果(非maven工程):
在这里插入图片描述
这里简单的说下报错原因是由于cglib在实现的时候依赖了一个asm的三方包,如果你是maven工程在有了这个依赖的情况下并不会报错,如果报错了可以导入下依赖即可,这里我提供下这个项目的依赖地址和jar包以供同学们使用。
pom依赖:

  	<dependency>
  		<groupId>org.ow2.asm</groupId>
  		<artifactId>asm</artifactId>
  		<version>5.0.4</version>
  	</dependency>

jar包下载地址:

链接:https://pan.baidu.com/s/1qizkN8rfw1YIrEZJ_8qKXw
提取码:2xeg

导入jar包后再次测试:
在这里插入图片描述
已经可以正常的实现动态代理的效果了,接下来我们简单的总结下cglib代理形式。

cglib的优缺点:
优点: 委托对象无需实现接口,较于接口代理形式更加灵活。
缺点: 由于使用了继承方式进行代理,因此被代理类不能被final修饰,目标方法不能被private和static修饰,限制了cglib的作用范围。

cglib与java自带的接口代理模式的区别

  1. Java动态代理只能够对接口进行代理,不能对普通的类进行代理(因为所有生成的代理类的父类为Proxy,Java类继承机制不允许多重继承);CGLIB能够代理普通类;
  2. Java动态代理使用Java原生的反射API进行操作,在生成类上比较高效;CGLIB使用ASM框架直接对字节码进行操作,在类的执行过程中比较高效

三、 代理模式总结

1. 代理模式的简单设计类图

在这里插入图片描述
代理模式主要是由三要素构成:

  1. 共同的行为(常用作接口或抽象类)
  2. 委托对象(真实对象,向代理对象提供委托)
  3. 代理对象(代理对象,负责实现真实对象的委托)

2. 代理模式的优缺点

优点:

  1. 可以灵活的为委托类实现管理和增强的目的,一定程度上降低了系统的耦合度。
  2. 在委派代理的时候,只需要针对代理对象进行扩展,符合开闭原则。

缺点:

  1. 一定程度上增加了系统的复杂度
  2. 使用代理模式后,可能会降低某些业务场景的执行效率

3. 代理模式的使用场景

菜鸟教程中提到过可以按照职责划分代理模式的用场景:
1、远程代理。
2、虚拟代理。
3、Copy-on-Write 代理。
4、保护(Protect or Access)代理。
5、Cache代理。
6、防火墙(Firewall)代理。
7、同步化(Synchronization)代理。
8、智能引用(Smart Reference)代理

4. 代理模式在Java中的应用

java中的代理模式真的是随处可见,比如Spring框架中的AOP技术(Aspect Oriented Programming,面向切面编程)就是使用代理模式实现的,其主要应用在日志,缓存,事务等业务模块的实现。使用AOP可以有效地减少业务与业务之间的耦合度。后续博主准备编写一个手写Spring框架的系列博文,当中也会重点的说一下代理模式在AOP中的相关应用。

6. 拓展:代理模式与装饰者模式的异同

不了解装饰者模式的同学请先学习一下装饰者模式的相关基础:
[设计模式] -装饰器模式

首先我们先比较一下两种模式的类图:
装饰者模式:
在这里插入图片描述
代理模式:
在这里插入图片描述

这里我们可以在类图上面发现两种模式的代码实现其实很相似,我们先说下两者的共同点:两者的共同点都是具有相同的接口,这里为了不让小伙伴疑惑,首先声明下,装饰模式就是代理模式的一种特殊应用。既然装饰模式属于代理模式,那么它具有代理模式的特点就很正常了。但是两者之间也存在一点差异,代理模式着重看重的是对代理过程的控制,而装饰模式更加关注对类功能的增强和减弱,他更加看中的是功能上的变化。所以小伙伴们就不要纠结与两者结构上的相似,毕竟代码写出来如果你不做注释并没有太多的同伴关注你使用的是装饰还是代理~

四、结语

今天的代理模式讲解就全部结束了,设计模式的相关代码已经在gitee上收录,有需求的小伙伴可以直接拿走:

https://gitee.com/xiaolong-oba/csdn-learning-code.git

有疑问的小伙伴欢迎评论区留言或者私信博主,博主会在第一时间为你解答。
码字不易,感到有收获的小伙伴记得要关注博主一键三连,不要当白嫖怪哦~
博主在这里祝大家可以在新的一年升职加薪,走上人生巅峰!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

晓龙oba

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值