0. 序
代理模式,在一些框架中频频见到的东西,得好好看看。
代理模式要做的:控制和管理访问。
代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
代理模式可以实现无侵入式的代码扩展,也就是方法增强,可以在不用修改源码的前提下进行增强方法。另外,通过代理的方式避免暴露被代理对象或者说代理不容易被取得的对象,满足开闭原则
1. 远程代理
远程代理即远程对象的本地代表。“远程对象”是指活在不同的JVM堆中(在不同的地址空间运行的远程对象)。"本地代表"是可以由本地方法调用的对象,其行为会转发到远程对象中。
客户对象所作的就像是在做远程方法调用,但其实置输调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信底层细节。
2. RMI(过时,了解)
RMI可以让我们找到远程JVM内的对象,并允许我们调用它们的方法。
RMI(Remote Method Invocation) – 远程方法调用。
RMI概览----本地通过调用代理对象,代理通过网络发送服务器上的远程对象,服务器上的辅助对象调用服务器本地的真正的服务对象的方法并将返回值封装并返回给本地的代理,本地代理将返回值返回给客户。
RMI提供了客户辅助对象与服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI的好处是你不必亲自写任何网络或I/O代码。客户程序调用远程方法(即真正的服务所在)就和在运行在客户本地JVM上对对象调用一样。
现在,将对象变成服务–接受远程调用;让客户远程调用
-
对象变成服务
五个步骤
-
制作远程接口:远程接口定义出可以让客户远程调用的方法。客户将用它作为服务类型。Stub(桩,指客户方的辅助对象)和实际的服务都实现此接口。
interface MyService{ void f1(); void f2(); }
-
制作远程实现:即实际工作的类,为远程接口中定义的远程方法提供了真正的实现。就是客户们想要真正调用的方法的对象。
class MyServiceImpl implements MyService{ //略 }
-
例用rmic产生stub(桩)和skeleton(骨架,指服务辅助对象) 不需要自己创建这些类,当运行rmic工具时,这些会自动处理,JDK中有rmic. eg:
rmic MyServiceImpl
-
启动RMI registry(rmiregistry): rmiregistry就像是电话本,客户可以从中查到代理的位置(也就是客户的stub helper对象)
-
开始远程服务:必须让服务器对象开始运行。服务器实现类会去实例化一个服务的实力,并将这个服务注册到RIM registry。 注册之后,这个服务可以供客户调用了。
rmi的方式已经过时,代码详细略。
-
3. 虚拟代理 virtual proxy
-
远程代理可以作为另一个JVM上对象的本地代表。调用代理的方法,会被代理利用网络转发到远程执行,并将结果通过网络返回给代理,再由代理返回结果给客户。如上述的RMI的远程调用。
-
虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要创建一个对象的时候才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
例子:在网络请求一个网站中,我们要得到一张图片,因为网络速度而不能把图片立即显示出来,因此等待图像加载的过程中,显示一些东西“正在加载…”,加载完成后就显示。那么这个“正在加载…”就是一个虚拟代理。
这一部分书上的代码不错,这里不搬运了 p494
保护代理:根据权限决定客户可否访问对象的代理。(根据动态代理构建)
例如一个商店,不允许职员对象设置工资值,只允许人力设置工资值。
4. 动态代理
动态的创建一个代理类,实现一个或多个接口,并将方法的调用转发到指定的类。
普通代理模式类图:
针对代理模式:
- 用户只关心接口功能,而不在乎谁提供功能。
- 接口真正实现的是RealSubject, 但它不直接与用户接触,而是通过代理
- 代理即Proxy, 由于实现了Subject接口,所以它能直接与用户接触
- 用户调用Proxy的时候,proxy内部调用了RealSubject, 所有Proxy是中介者,它可以增强RealSubject
动态代理模式类图:
动态代理的使用:
例用Java API来创建InvocationHandler
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean personBean;
public OwnerInvocationHandler(PersonBean personBean) {
this.personBean = personBean;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().startsWith("get")) {
return method.invoke(personBean, args);
} else if (method.getName().equals("setHotOrNotRating")) {
//不允许自己改变自己的rating
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
return method.invoke(personBean, args);
}
return null;
}
}
如上创建自己的invocation,只允许自己修改部分数据,控制访问权限。
PersonBean getOwnerProxy(PersonBean personBean){
return (PersonBean) Proxy.newProxyInstance(personBean.getClass().getClassLoader()
,personBean.getClass().getInterfaces(),new OwnerInvocationHandler(personBean));
}
如上创建动态代理。
动态代理的“动态”? --> 是运行时才有Proxy类,它是根据传入的接口创建的。
静态代理:即在代码中写好了代理类,就如书中CD部分的代码,是Proxy类,动态代理只需要创建invocationHandler, 代理类Proxy在运行时生成。
下面拿一个实例来说明动态代理的好处
静态代理–电影院插播广告
/**
* 电影播放接口
* @author chain
* @date 2020/4/17
*/
public interface MoviePlay {
/**
* 电影播放方法
* @author chain
* @date 2020/4/17
*/
void play();
}
/**
* @author chain
* @description 真实的类 播放电影
* @date 2020/4/17
*/
public class RealSubjectMovie implements MoviePlay{
@Override
public void play() {
System.out.println("当前正在播放电影<< 拯救大兵瑞恩 >>");
}
}
package 代理模式.动静态代理;
/**
* @author chain
* @description 影院代理 它来控制电影的播放 并对播放方法进行增强
* @date 2020/4/17
*/
public class CinemaProxy implements MoviePlay {
/**
* 代理要控制真实对象的访问,必然有依赖关系
*/
RealSubjectMovie realSubjectMovie;
public CinemaProxy(RealSubjectMovie realSubjectMovie) {
this.realSubjectMovie = realSubjectMovie;
}
/**
* 对这个方法进行增强,插入广告
*
* @author chain
* @date 2020/4/17
*/
@Override
public void play() {
startAds();
//真实对象调用方法
realSubjectMovie.play();
endAds();
}
public void startAds() {
System.out.println("下面电影即将开始,请持票顾客马上进入影厅...");
}
public void endAds() {
System.out.println("电影已经结束,请有序离场...");
}
public static void main(String[] args) {
//测试方法
RealSubjectMovie realSubjectMovie = new RealSubjectMovie();
MoviePlay cinema = new CinemaProxy(realSubjectMovie);
cinema.play();
}
}
//效果如下
下面电影即将开始,请持票顾客马上进入影厅...
当前正在播放电影<< 拯救大兵瑞恩 >>
电影已经结束,请有序离场...
在这个静态例子中,我们要手动编写代码让Cinema实现MoviePlay接口
动态代理–电影院播放电影,播放音乐,买卖产品
package 代理模式.动静态代理.动态;
/**
* @author chain
* @description 免费电影播放类
* @date 2020/4/17
*/
public class FreeMoviePlay implements MoviePlay{
@Override
public void play() {
System.out.println("当前播放的是免费电影---《逐梦演艺圈》");
}
}
package 代理模式.动静态代理.动态;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @author chain
* @description 调用实现类invocation 它来决定这个调用如何处理
* @date 2020/4/17
*/
public class CinemaInvocation implements InvocationHandler {
private Object movie;
public CinemaInvocation(Object movie) {
this.movie = movie;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("开始播放电影,播放影院是 "+ this.getClass().getSimpleName());
method.invoke(movie,args);
System.out.println("播放结束...");
return null;
}
public static void main(String[] args) {
MoviePlay freeMovie=new FreeMoviePlay();
InvocationHandler cinema1=new CinemaInvocation(freeMovie);
MoviePlay moviePlayProxy=(MoviePlay) Proxy.newProxyInstance(freeMovie.getClass().getClassLoader(),
freeMovie.getClass().getInterfaces(),cinema1);
moviePlayProxy.play();
}
}
//测试
开始播放电影,播放影院是 CinemaInvocation
当前播放的是免费电影---《逐梦演艺圈》
播放结束...
ok, 这里没有去实现MoviePlay的接口,但仍然实现了相同的功能。
但是有了invocationHandler, InvocationHandler 是一个接口,官方文档解释说,每个代理的实例都有一个与之关联的 InvocationHandler 实现类,如果代理的方法被调用,那么代理便会通知和转发给内部的 InvocationHandler 实现类,由它决定处理。因为,Proxy 动态产生的代理会调用 InvocationHandler 实现类,所以 InvocationHandler 是实际执行者。
现在开始播放付费电影。
/**
* @author chain
* @description 付费电影播放
* @date 2020/4/17
*/
public class PaidMoviePlay implements MoviePlay{
@Override
public void play() {
System.out.println("当前播放付费电影---<<禁闭岛>>");
}
}
public static void main(String[] args) {
//realSubject
MoviePlay freeMoviePlay=new FreeMoviePlay();
MoviePlay paidMoviePlay=new PaidMoviePlay();
//执行者
InvocationHandler fIH=new CinemaInvocation(freeMoviePlay);
InvocationHandler pIH=new CinemaInvocation(paidMoviePlay);
//动态产出代理
MoviePlay fProxy=(MoviePlay) Proxy.newProxyInstance(freeMoviePlay.getClass().getClassLoader(),
freeMoviePlay.getClass().getInterfaces(),fIH);
MoviePlay pProxy=(MoviePlay) Proxy.newProxyInstance(freeMoviePlay.getClass().getClassLoader(),freeMoviePlay.getClass().getInterfaces(),
pIH);
//测试
fProxy.play();
pProxy.play();
}
//测试结果
开始播放电影,播放影院是 CinemaInvocation
当前播放的是免费电影---《逐梦演艺圈》
播放结束...
开始播放电影,播放影院是 CinemaInvocation
当前播放付费电影---<<禁闭岛>>
播放结束...
这里pProxy , fProxy
是没有本质区别的,都是电影播放的接口的子类。
现在,电影院要卖饮料–>
package 代理模式.动静态代理.动态;
/**
* @author chain
* @description 卖wahaha
* @date 2020/4/17
*/
public class WahahaSellWater implements SellWater {
@Override
public void sell() {
System.out.println("这里在卖wahaha, 2r一瓶");
}
}
//测试 省略main
//卖水测试
SellWater sellWater=new WahahaSellWater();
InvocationHandler wIH=new CinemaInvocation(sellWater);
SellWater sProxy=(SellWater) Proxy.newProxyInstance(sellWater.getClass().getClassLoader(),
sellWater.getClass().getInterfaces(),wIH);
sProxy.sell();
//结果
开始播放电影,播放影院是 CinemaInvocation
这里在卖wahaha, 2r一瓶
播放结束...
tips: 在卖水的过程中出现了 播放电影… , 可以在invoke()中添加if-else结构来选择执行的增强方法。
eg:
if(放电影){
System.out.println("开始播放电影,播放影院是 "+ this.getClass().getSimpleName());
method.invoke(movie,args);
System.out.println("播放结束...");
}else{
method.invoke(movie,args);
}
对比两个例子 如果使用静态代理,那么我每增加一个新的功能,都要对CinemaProxy
中进行修改,增加真实对象的访问,并对调用方法进行if-else判断,代码的弹性低,或者手动为每一个目标类都编写对应的代理类。使用动态代理,我只需要新增加invocationHandler
即可,对之前的代码没有影响,而invocationHandler
让我们可以只专注于增强代码的编写。
另外,像spring框架之类的,不可能对业务中进行静态代理编写,只会进行动态代理增强。例如上述的放电影实例,CinemaInvocation中的注入是一个Objcet, 就可以通过动态代理来处理放电影,卖水等业务逻辑。
业务代码内,当需要增强的业务逻辑非常通用(如:添加log,重试,统一权限判断等)时,使用动态代理将会非常简单,如果每个方法增强逻辑不同,那么静态代理更加适合。
使用静态代理时,如果代理类和被代理类同时实现了一个接口,当接口方法有变动时,代理类也必须同时修改。