1.引入
1.0 联系JAVA知识
Java中的? 动态代理 和 静态代理? 这两个知识点也就是代理模式的知识点。
1.1 需求引入
接着第10张 状态模式的例子接着向下进行:
糖果机已经可以取得糖果数量getCount()方法和取得糖果机状态的getState()方法。
现在所需要的事,就是创建一份能打印出来的报告,然后将它递送给CEO。
我们可能需要为每个糖果机加上一个位置的字段。
1.2 初步尝试
首先为GumballMachine加上处理位置的支持:
package designMode.proxy.gumballmonitor;
public class GumballMachine {
//其他实例变量
String location;
public GumballMachine(String location, int count) {
//构造器内的其他代码
this.location = location;
}
public String getLocation() {
return location;
}
//其他方法
}
然后,再创建另一个类,GumballMonitor(糖果监视器),以便取得机器的位置、糖果的库存量以及当前机器的状态,并打印成一份可爱的报告。
package designMode.proxy.gumballmonitor;
public class GumballMonitor {
GumballMachine machine;
//监视器的构造器需要被传入糖果机,它会将糖果机记录在machine实例变量中
public GumballMonitor(GumballMachine machine) {
this.machine = machine;
}
public void report() {
System.out.println("Gumball Machine: " + machine.getLocation());
System.out.println("Current inventory: " + machine.getCount() + " gumballs");
System.out.println("Current state: " + machine.getState());
}
}
测试监视器:
package designMode.proxy.gumballmonitor;
public class GumballMachineTestDrive {
public static void main(String[] args) {
int count = 0;
//利用命令函传入位置和一开始的糖果数目
if (args.length < 2) {
System.out.println("GumballMachine <name> <inventory>");
System.exit(1);
}
try {
count = Integer.parseInt(args[1]);
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
//将位置和数量,传入糖果机器gumballMachine的构造器
GumballMachine gumballMachine = new GumballMachine(args[0], count);
//将糖果机实例传入 监视器monitor实例
GumballMonitor monitor = new GumballMonitor(gumballMachine);
System.out.println(gumballMachine);
//其他测试代码
monitor.report();
}
}
输出:
? 这虽然实现了,但是不太满足 需求。 需要的是 远程监控糖果机。
1.3 进一步分析需求
要实现的是? 远程监控 糖果机。
通过这个需求,可知 应该通过远程代理来实现。?
之前的代码 不能实现远程,因为是给GumballMonitor一个糖果机的引用,它给我们一份报告,问题在于监视器和糖果机在同一个JVM上执行。
但是CEO希望他在他的桌面上远程监控这些机器。因此,可以不变化GumballMonitor,不要将糖果机交给GumballMonitor,而是将一个远程对象的代理交给他。
所谓的代理,就是代表某个真实的对象。在这个案例中,代理就像是糖果机对象一样,但其实幕后就是它利用网络和一个远程的真正糖果机沟通。
因此,不需要改变代码,只需要将GumballMachine代理版本的引用交给监视器,然后这个代理假装它是真正的对象,但其实一切的动作是它利用网络和真正的对象沟通。
Java已经有一些很棒的内置工具可以帮助实现远程代理。
1.4 远程代理的角色
远程代理就好比“远程对象的本地代表”。
何谓“远程对象”?这是一种对象,活在不同的java虚拟机(JVM)堆中。
何谓“本地代表”?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。
在CEO桌面的本地堆,糖果监视器是客户对象,它以为沟通的对象是真正的糖果机,但政治与他沟通的是代理,再由代理通过网络和真正的糖果机沟通。
代理可以假装自己是远程对象,但其实只是一个中间的角色。
客户对象所做的就像是在做远程方法调用,但其实只是调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信的低层细节。
2.了解RMI
相关参考链接:
从懵逼到恍然大悟之Java中RMI的使用_lmy86263的博客-CSDN博客_rmi
java RMI原理详解_xinghun_4的专栏-CSDN博客_rmi
2.1 远程方法工作要点
RMI(Java?Remote Method Invocation,Java 远程方法调用).
如何创建一个代理,来调用在另一个JVM中的对象的方法?
不能这么写:Duck d=<另一个堆的对象>
变量d只能引用当前代码语句的同一堆空间的对象。那该怎么办?该是Java远程方法调用出现的时刻了...
RMI可以让我们找到远程JVM内的对象,并允许我们调用他们的方法。
远程方法工作要点
假如我们想要设计一个系统,能够调用本地对象,然后将每个请求转发到远程对象上进行。要如何设计?
我们需要一些辅助对象,帮我们真正进行沟通。这些辅助对象使客户就像在调用本地对象的方法(事实也是如此)一样。客户调用客户辅助对象上的方法,仿佛客户辅助对象就是真正的服务。客户辅助对象再负责为我们转发这些请求。
换句话说,客户对象以为它调用的是远程服务上的方法,因为客户辅助对象乔装成服务对象,假装自己有客户所要调用的方法。但是客户辅助对象不是真正的远程服务。虽然操作看起来很像(因为具有服务所宣称的相同的方法),但是并不真正拥有客户所期望的方法逻辑。客户辅助对象会联系服务器,传送方法调用信息(例如,方法名称、变量等),然后等待服务器的返回。在服务器端,服务辅助对象从客户辅助对象中接收请求(透过Socket连接),将调用的信息解包,然后调用真正服务对象上的真正方法。
所以,对于服务对象来说,调用是本地的,来自服务辅助对象,而不是远程客户。服务辅助对象从服务中得到返回值,将它打包,然后运回到客户辅助对象(通过网络Socket的输出流),客户辅助对象对信息解包,最后将返回值交给客户对象。
?
远程方法调用是如何发生的?
?1.客户对象调用客户辅助对象的doBigThing()方法
2.客户辅助对象打包调用(变量、方法名称等),然后通过网络将它运给服务辅助对象
?3.服务辅助对象把来自客户辅助对象的信息解包,找出被调用的方法(以及在哪个对象内),然后调用真正的服务对象上的真正方法。
?4.服务对象上的方法被调用,将结果返回给服务辅助对象
?5.服务辅助对象把调用的返回信息打包,然后通过网络运回给客户辅助对象
6.客户辅助对象把返回值解包,返回给客户对象。对于客户来说,这是完全透明的。
?2.2 RMI概观
已经知道远程方法如何工作的要点,还需要了解如何利用RMI进行远程方法调用。
RMI提供了客户辅助对象和服务辅助对象,为客户辅助对象创建和服务对象相同的方法。RMI的好处在于你不必亲自写任何网络或I/O代码。客户程序调用远程方法(即真正的服务所在)就和在运行在客户自己的本地JVM上对对象进行正常方法调用一样。
RMI也提供了所有运行时的基础设施,好让这一切正常工作。这包括了查找服务(lookup service),这个服务用来寻找和访问远程对象。
关于RMI调用和本地(正常的)方法调用,有一个不同点。虽然调用远程方法就如同调用本地方法一样,但是客户辅助对象会通过网络发送方法调用,所以网络和I/O的确是存在的。
我们知道网络和/O是有风险的,容易失败的,所以随时都可能抛出异常,也因此,客户必须意识到风险的存在。再过几页我们就会讨论这部分。
RMI称呼(译注:terminology,术语,重点在概念本身,nomenclature,称呼,重点在概念上贴的标签):RMI将客户辅助对象称为stub(桩),服务辅助对象称为skeleton(骨架)。
?现在,我们就来看看如何将对象变成服务——可以接受远程调用的服务。也看看,如何让客户做远程调用。 ?
2.3 制作远程服务
制作远程服务的5个步骤的概要。这些步骤将一个普通的对象变成可以被远程客户调用的远程对象。稍后会将这些步骤应用到GumballMachine。现在来看看这些步骤的细节:
步骤一: 制作远程接口
远程接口定义出可以让客户远程调用的方法。客户将用它作为服务的类类型。Stub和实际的服务都实现此接口。
步骤二:制作远程的实现
这是做实际工作的类,为远程接口中定义的远程方法提供了真正的实现。这就是客户真正想要调用方法的对象。(例如,我们的GumballMachine)。
步骤三:利用rmic产生的stub和skeleton
这就是客户和服务的辅助类。你不需要自己创建这些类,甚至连生成他们的代码都不用看,因为当你运行rmic工作时,会自动处理。你可以在JDK中找到rmic.
步骤四:启动RMI registry(rmiregistry)
rmireistry就像是电话簿,客户可以从中查到代理的位置(也就是客户的stub helper对象)。
步骤五:开始远程服务
你必须让服务对象开始运行。你的服务实现类会去实例化一个服务的实例,并将这个服务注册到RMIregistry。注册之后,这个服务就可以供客户调用了。 ?
2.4 实现远程服务
2.4.1 步骤一:制作远程接口
1.扩展java.rmi.Rmote
Remote是一个“记号”接口,所以Remote不具有方法。对于RMI来说,Remote接口具有特别的意义,因此要遵守规则。请注意,这里说的是“扩展”(extends),因为接口可是“扩展”另一个接口
public interface MyRemote extends Remote {
}
2.声明所有的方法都会抛出RemoteException
客户使用远程接口调用服务。换句话说,客户调用实现远程接口的Stub上的方法,而Stub底层用到了网络和I/O,所以各种坏事都可能会发生。客户必须认识到风险,通过处理或声明远程异常来解决。如果接口中的方法声明了异常,任何在接口类型的引用上调用方法的代码也必须处理或声明异常。
package designMode.proxy.gumballmonitor.rmiTest;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* @author myl
* @create 2022-03-12 13:37
*/
public interface MyRemote extends Remote {
//每次远程方法调用都必须考虑成是“有风险的”。
//在每个方法中声明 RemoteException,可以让客户注意到这件事,并了解这是可能无法工作的
public String sayHello() throws RemoteException;
}
3.确定变量和返回值是属于原语(primitive)类型或者可序列化(Serializable)类型
远程方法的变量和返回值,必须属于原语类型或者Serializable类型。这不难理解。远程方法的变量必须被打包并通过网络运送,这要靠序列化来完成。如果你使用原语类型、字符串和许多API中内定的类型(包括数组和集合),都不会有问题。如果你定义自己定义的类,就必须要保证你的类实现了Serializble。
?2.4.2 步骤二:制作远程实现
1.实现远程接口
你的服务必须实现远程接口,也就是客户将要调用的方法的接口。
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
public MyRemoteImpl() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Server says Hey";
}
}
2.扩展UnicastRemoteObject
为了要成为远程服务对象,你的对象需要某些“远程的”功能。最简单的方法是扩展java.rmi.server.UnicastRemoteObject,让超类帮你做这些工作。
3.设计一个不带变量的构造器,并声明RemoteException
你的新超类UnicastRemoteObject带来一个小问题:它的构造器抛出RemoteException。唯一解决这个问题的方法就是为你的远程实现声明一个构造器、这样就有了个声明RemoteException的地方。当类被实例化的时候,超类的构造器总是会被调用。如果超类的构造器抛出异常,那么你只能声明子类的构造器也抛出异常。
//不需要在构造器中放进任何代码,只需要有办法声明超类构造器会抛出异常。
public MyRemoteImpl() throws RemoteException {
}
4.使用RMI Registry注册此服务
现在你已经有一个远程服务了,必须让它可以被远程客户调用。你要做的是将此服务实例化,然后放进RMl registry中(记得先确定RMI Registry正在运行,否则注册会失败)。当注册这个实现对象时,RMI系统其实注册的是stub,因为这是客户真正需要的。注册服务使用了java.rmi.Naming类的静态rebind()方法。
2.4.3 步骤三:产生Stub和Skeleton
在远程实现类(不是远程接口)上执行rmic
rmic是JDK内的一个工具,用来为一个服务类产生stub和skeleton。命名习惯是在远程实现的名字后面加上_Stub或_Skel。rmic有一些选项可以调整,包括不要产生skeleton、查看源代码,甚至使用IIOP作为协议。我们这里使用rmic的方式是常用的方式,将类产生在当前目录下(就是你cd到的地方)。请注意,rmic必须看到你的实现类,所以你可能会从你的远程实现所在的目录执行rmic (为了简单起见,我们这里不用package。但是在真实世界中,你必须注意packagc的目录结构和名称问题)。
2.4.4 步骤四:执行remiregistry
开启一个终端,启动remiregistry
先确定启动目录必须可以访问你的类。最简单的做法是从你的"classed"目录启动
2.4.5 步骤五:启动服务
开启另一个终端,启动服务
从哪里启动?可能是从你的远程实现类中的main()方法,也可能是从一个独立的启动类。在这个简单的例子中,我们是从实现类中的main()方法启动的,先实例化一个服务对象,然后到RMI registry中注册。 ?
2.4.6 服务端完整的代码
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
/**
* @author myl
* @create 2022-03-12 14:24
*/
//想要创建一个远程对象,扩展UnicastRemoteObject是最容易的方法
public class MyRemoteImpl extends UnicastRemoteObject implements MyRemote {
//你的超类(UnicastRemoteObject)构造器声明了一次,所以你必须写一个构造器
//因为这意味着你的构造器正在调用不安全的代码(它的超类构造器)
public MyRemoteImpl() throws RemoteException {
}
//必须实现远程接口。当然你必须要实现所有的接口方法,但是请注意,不需要声明RemoteException
@Override
public String sayHello() {
return "Server says Hey";
}
public static void main(String[] args) {
//先产生远程对象,在使用Naming.rebind()绑定到rmiregistry.
// 客户将使用你所注册的名称在RMI registry中寻找他。
try{
MyRemote service=new MyRemoteImpl();
Naming.rebind("RemoteHello",service);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
2.5 客户如何取得stub对象
2.5.1 工作方式
客户必须取得stub对象(我们的代理)以调用其中的方法。所以我们就需要RMI Registry的帮忙。客户从Registry中寻找(lookup)代理,就好像在电话簿里寻找一样,说:“我要找这个名字的stub。” 我们现在就来看看那些我们需要寻找并取得某个stub对象的代码。
工作步骤:
1.客户到RMI registry中寻找
2.RMI registry返回Stub对象
?(作为lookup方法的返回值)然后RMI会自动对stub反序列化。你在客户端必须有stub类(由rmic为你产生),否则stub就无法被反序列化。
3.客户调用stub的方法,就像stub就是真正的服务对象一样
2.5.2 完整的客户端代码
package designMode.proxy.gumballmonitor.rmiTest;
import java.rmi.Naming;
/**
* @author myl
* @create 2022-03-12 17:26
*/
public class MyRemoteClient {
public static void main(String[] args) {
new MyRemoteClient().go();
}
public void go() {
try{
//因为返回的是Object类型,所以别忘了转换类型
MyRemote service=(MyRemote) Naming.lookup("rmi://127.0.0.1/RemoteHello");
String s=service.sayHello();
System.out.println(s);
}catch (Exception ex){
ex.printStackTrace();
}
}
}
2.5.3 实验
对该部分暂不实验
3.实现GumballMachine远程代理
3.1 GumballMachine套用RMI框架
?3.2 让GumballMachine准备好当一个远程服务
3.2.1 步骤分析
要将代码改成使用代理,第一个步骤是让GumballMachine变成可以接受远程调用。换句话说,要将它变成一个服务。做法如下:
1.为GumballMachine创建一个远程接口,该口提供了一组可以远程调用的方法
2.确定接口的所有返回类型都是可以序列化的
3.在一个具体类中,实现此接口
3.2.2 逐步实现远程服务GumballMachine
先从远程接口开始:
public interface GumballMachineRemote extends Remote {
//所有返回类型都必须是原语类型或可序列化类型
//这些是准备支持的方法,每个都要抛出RemoteException
public int getCount() throws RemoteException;
public String getLocation() throws RemoteException;
public State getState() throws RemoteException;
}
由于State类不是可序列化的,因此要进行修改:
public interface State extends Serializable {
public void insertQuarter();
public void ejectQuarter();
public void turnCrank();
public void dispense();
}
实际上,我们还没处理完Serializable。对于State,我们有一个问题。每个状态对象都维护着一个对糖果机的引用,这样一来,状态对象就可以调用糖果机的方法,改变糖果机的形状。我们不希望整个糖果机都被序列化并随着State对象一起传送。修正这点:
public class NoQuarterState implements State {
//对于State的每个实现,在每个GumballMachine实例变量前加上transient关键字,这样就告诉JVM不要序列化这个字段
transient GumballMachine gumballMachine;
//...
}
已经实现GumballMachine类,但是要确定它可以当成服务事业,并处理来自网络上的请求。为了做到这一点,我们必须确定GumballMachine实现GumballMachineRemote接口。
package designMode.proxy.gumballRemote;
import java.rmi.*;
import java.rmi.server.*;
/**
* GumballMachine类要成为远程服务,并处理来自网络上的请求,需要进行以下几点:
* 1.GumballMachine要继承UnicastRemoteObject,成为一个远程服务
* 2.GumballMachine也要实现远程接口GumballMachineRemote
*
**/
public class GumballMachine
extends UnicastRemoteObject implements GumballMachineRemote
{
//实例变量...
//构造器需要抛出RemoteException,因为超类是这么做的
public GumballMachine(String location, int numberGumballs) throws RemoteException {
//...
}
//...
}
3.2.3 在RMI registry中注册
糖果机服务已经完成了。现在,我们要将它装上去,好开始接受请求。首先,要确保将它注册到RMI registry中,好让客户可以找到它、
package designMode.proxy.gumballRemote;
import java.rmi.*;
public class GumballMachineTestDrive {
public static void main(String[] args) {
GumballMachineRemote gumballMachine = null;
int count;
if (args.length < 2) {
System.out.println("GumballMachine <name> <inventory>");
System.exit(1);
}
try {
count = Integer.parseInt(args[1]);
gumballMachine =
new GumballMachine(args[0], count);
Naming.rebind("//" + args[0] + "/gumballmachine", gumballMachine);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后开始执行:
现在命令行运行:
然后再去运行:
??
3.2.4 编写GumballMachine客户端
对GumballMachine做出一些改变:
package designMode.proxy.gumballRemote;
import java.rmi.*;
public class GumballMonitor {
//现在准备依赖此远程接口,而不是具体的GumballMachine类
GumballMachineRemote machine;
public GumballMonitor(GumballMachineRemote machine) {
this.machine = machine;
}
public void report() {
try {
System.out.println("Gumball Machine: " + machine.getLocation());
System.out.println("Current inventory: " + machine.getCount() + " gumballs");
System.out.println("Current state: " + machine.getState());
} catch (RemoteException e) {
e.printStackTrace();
}
}
}
3.2.5 再编写监视器测试程序
现在在写一些代码,让CEO可以监控许多糖果机:
import java.rmi.Naming;
public class GumballMonitorTestDrive {
public static void main(String[] args) {
//被见识的位置有这些,需要创建数组来保存机器人的位置
String[] location = {"rmi://127.0.0.1/gumballmachine"};
GumballMonitor[] monitor = new GumballMonitor[location.length];
//为每个远程机器创建一个代码
for (int i=0;i < location.length; i++) {
try {
GumballMachineRemote machine =
(GumballMachineRemote) Naming.lookup(location[i]);
monitor[i] = new GumballMonitor(machine);
System.out.println(monitor[i]);
} catch (Exception e) {
e.printStackTrace();
}
}
for (int i=0; i < monitor.length; i++) {
monitor[i].report();
}
}
}
运行客户端(监控端)之后,就可以直接与服务端相连接了:
3.2.6 技术注意点
在本地实现,需要对书上的代码进行修改。比如监听的IP,需要修改为“127.0.0.1”
对于在命令行使用java指令执行已经编译完成的xxx.class文件
因为原先项目中的Module太多,直接编制出现“找不到加载。。。”,因此直接创建了个新的空白项目,然后编译在执行的。
要获取.class文件,在IDEA上的操作,参考?idea编译Java项目的部分java文件并且生成class文件_拉里拉的博客-CSDN博客_idea编译单个java文件
3.3 实现的流程图
1.CEO执行监视器,先取得远程糖果机的代理,然后调用每个代理的getState()(以及getCount()和getLocation())
2.代理商的getState()被调用,此调用被转发到远程服务.Skeleton接收到请求,然后转发给糖果机。
?
3.糖果机将状态返回给skeleton,skeleton将状态序列化,通过网络传回给代理,代理将其反序列化,将它当做一个对象返回给监视器。
?远程代理模式是一般代理模式的一种实现,这个模式的变体相当多。
4.代理模式的定义
4.1 定义代理模式
代理模式定义:代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
使用代理模式创建代表( representative)对象,让代表对象控制某对象的访问,被代理的对象可是远程的对象、创建开销大的对象或需要安全控制的对象。
在糖果机的例子中,代理控制了对远程对象的访问。代理之所以需要控制访问,是因为我们的客户(监视器)不知道如何和远程对象沟通。从某个方面来看,远程代理控制访问,好帮我们处理网络上的细节。正如同刚刚说过的,代理模式有许多变体,而这些变体几乎都和“控制访问”的做法有关。
稍后我们会对此讨论得更详细,目前我们还是先看看几种代理控制访问的方式:
- 就像我们已经知道的,远程代理控制访问远程对象。
- 虚拟代理控制访问创建开销大的资源。
- 保护代理基于权限控制对资源的访问。
4.2 类图
Subject<<interface>>:Proxy和RealSubject都实现了Subject接口,这允许任何客户都可以想处理RealSubject对象一样处理proxy对象
Proxy:Proxy持有Subject的引用,所以必要时,它可以将请求转发给Subject.
RealSubject:RealSubject通常是真正做事的对象,proxy会控制对RealSubject的访问。
首先是Subject,它为RealSubject和Proxy提供了接口。通过实现同一接口,Proxy在RealSubject出现的地方取代它。
RealSubject是真正做事的对象,它是被proxy代理和控制访问的对象。
Proxy持有RealSubject的引用。在某些例子中,Proxy还会负责RealSubject对象的创建与销毁。客户和RealSubject的交互都必须通过Proxy。因为Proxy和RealSubject实现相同的接口(Subject),所以任何用到RealSubject的地方,都可以用Proxy取代。Proxy也控制了对RealSubject的访问,在某些情况下,我们可能需要这样的控制。这些情况包括RealSubject是远程的对象、RealSubject创建开销大,或RealSubject需要被保护。 ?
接下来对代理模式的各种用法进行介绍。
5.虚拟代理
5.1 虚拟代理与远程代理的区别
远程代理:远程代理可以作为另一个JVM上对象的本地代表。调用代表的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转回给客户。?
虚拟代理:虚拟代理作为创建开销大的对象的代表。虚拟代理经常直到我们真正需要一个对象的时候,才创建它。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。
?5.2 需求引入
我们打算建立一个应用程序,用来展现你最喜欢的CD封面。你可以建立一个CD标题菜单,然后从Amazon.com等网站的在线服务中取得CD封面的图。如果你使用Swing,可以创建一个Icon接口从网络上加载图像。唯一的问题是,限于连接带宽和网络负载,下载可能需要一些时间,所以在等待图像加载的时候,应该显示一些东西。我们也不希望在等待图像时整个应用程序被挂起。一旦图像被加载完成,刚才显示的东西应该消失,图像显示出来。 想做到这样,简单的方式就是利用虚拟代理。虚拟代理可以代理Icon,管理背景的加载,并在加载未完成时显示“CD封面加载中,请稍候……”,一旦加载完成,代理就把显示的职责委托给Icon。
5.3 图像代理控制访问
在开始写CD封面浏览器代码之前,先看一下类图。此处的代理是用于隐藏创建开销大的对象,而不是隐藏在网络其他地方的对象。?
?ImageProxy是如何工作的:
1.ImgaeProxy首先创建一个ImageIcon,然后开始从网络URL上加载图像
2.在加载的过程中,ImgaeProxy显示“CD封面加载中,请稍后...”
3.当图像加载完毕,ImageProxy把所有方法调用委托给真正的ImageIcon,这些方法包括了paintIcon()、getWidth()和getHeight().
4.如果用户请求新的图像,我们就创建新的代理,重复这样的过程。
5.4 编写ImgaeProxy
package virtualproxy;
import java.net.*;
import java.awt.*;
import javax.swing.*;
class ImageProxy implements Icon {
//imageIcon是我们希望在加载后显示出来的真正的图像
volatile ImageIcon imageIcon;//实际对象 RealSubject
final URL imageURL;
Thread retrievalThread;
boolean retrieving = false;
//将图像的URL传入构造其中,这是我们希望显示的图像所在的位置
public ImageProxy(URL url) { imageURL = url; }
//在图像加载完毕前,返回默认的宽和高,图像加载完毕后,转给imageIcon处理
@Override
public int getIconWidth() {
if (imageIcon != null) {//如果实际对象加载好了,虚拟代理直接将请求委托给实际对象
return imageIcon.getIconWidth();
} else {//如果实际对象还没加载好,就返回虚拟代理设置的东西
return 800;
}
}
@Override
public int getIconHeight() {
if (imageIcon != null) {//同理于上
return imageIcon.getIconHeight();
} else {
return 600;
}
}
synchronized void setImageIcon(ImageIcon imageIcon) {
this.imageIcon = imageIcon;
}
@Override
public void paintIcon(final Component c, Graphics g, int x, int y) {
if (imageIcon != null) {//同理于上,如果realsubject已经创建出来了,就会通过委托,让实际对象imageIcon画出自己
imageIcon.paintIcon(c, g, x, y);
} else {//如果没有,那么就由虚拟代理对象自己创建一个
//否则,就显示那个“加载中”的消息
g.drawString("Loading album cover, please wait...", x+300, y+190);
if (!retrieving) {
/**在这里加载真正的icon图像
*如果不新开个线程的话,那么加载图像和ImageIcon是同步的
*因此 新开一个线程来让加载变成异步的
**/
retrieving = true;
retrievalThread = new Thread(new Runnable() {
@Override
public void run() {
try {
//在线程中,我们实例化此Icon对象,其构造器会在图像加载完成后才返回
setImageIcon(new ImageIcon(imageURL, "Album Cover"));
c.repaint();//当图像准备好时,我们告诉swing,需要重绘
} catch (Exception e) {
e.printStackTrace();
}
}
});
retrievalThread.start();
}
}
}
}
5.5 测试CD封面浏览器
package virtualproxy;
import java.net.*;
import javax.swing.*;
import java.util.*;
public class ImageProxyTestDrive {
ImageComponent imageComponent;
JFrame frame = new JFrame("Album Cover Viewer");
JMenuBar menuBar;
JMenu menu;
Hashtable<String, String> albums = new Hashtable<String, String>();
public static void main (String[] args) throws Exception {
ImageProxyTestDrive testDrive = new ImageProxyTestDrive();
}
public ImageProxyTestDrive() throws Exception {
albums.put("Buddha Bar","http://images.amazon.com/images/P/B00009XBYK.01.LZZZZZZZ.jpg");
albums.put("Ima","http://images.amazon.com/images/P/B000005IRM.01.LZZZZZZZ.jpg");
albums.put("Karma","http://images.amazon.com/images/P/B000005DCB.01.LZZZZZZZ.gif");
albums.put("MCMXC a.D.","http://images.amazon.com/images/P/B000002URV.01.LZZZZZZZ.jpg");
albums.put("Northern Exposure","http://images.amazon.com/images/P/B000003SFN.01.LZZZZZZZ.jpg");
albums.put("Selected Ambient Works, Vol. 2","http://images.amazon.com/images/P/B000002MNZ.01.LZZZZZZZ.jpg");
URL initialURL = new URL((String)albums.get("Selected Ambient Works, Vol. 2"));
menuBar = new JMenuBar();
menu = new JMenu("Favorite Albums");
menuBar.add(menu);
frame.setJMenuBar(menuBar);
for (Enumeration<String> e = albums.keys(); e.hasMoreElements();) {
String name = (String)e.nextElement();
JMenuItem menuItem = new JMenuItem(name);
menu.add(menuItem);
menuItem.addActionListener(event -> {
imageComponent.setIcon(new ImageProxy(getAlbumUrl(event.getActionCommand())));
frame.repaint();
});
}
// set up frame and menus
Icon icon = new ImageProxy(initialURL);
imageComponent = new ImageComponent(icon);
frame.getContentPane().add(imageComponent);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800,600);
frame.setVisible(true);
}
URL getAlbumUrl(String name) {
try {
return new URL((String)albums.get(name));
} catch (MalformedURLException e) {
e.printStackTrace();
return null;
}
}
}
效果图:
5.6 流程总结
问题:远程服务器和虚拟服务器差异非常大,他们真的是一个模式吗?
回答:在真实的世界中,代理模式有许多变体,这些变体都有共同点:都会将客户对主体(subject)施加的方法调用拦截下来。这种间接的级别让我们可以做许多事,包括将请求分发到远程主体;给创建开销大的对象提供达标;或者正如你将要看到的,提供某些级别的保护,这种保护能决定哪些客户能调用那些方法。
6.保护代理
6.1 Java API的代理
Java在java.lang.reflect包中有自己的代理支持,利用这个包可以在运行时动态创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。因为实际的代理类是在运行时创建的,我们称这个Java技术为:动态代理。
我们要利用Java的动态代理创建我们下一个代理实现(保护代理)。但在这之前,先看一下类图,了解动态代理:
?Proxy:Proxy是由Java产生的,而且实现了完整的Subject接口.
InvocationHandler: 你提供InvocationHandler,Proxy上的任何方法调用都会被传入此类。InvocationHandler控制对RealSubject方法的访问,InvocationHandler实现了代理的行为。
因为Java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy不是你直接实现的。既然这样的代码不能放在Proxy类中,那么要放在哪里?放在InvocationHandler中。InvocationHandler的工作是响应代理的任何调用。你可以把InvocationHandler想成是代理收到方法调用后,请求做实际工作的对象。 其调用顺序就是 向Proxy发起请求,然后由InvocationHandler进行响应,并调用RealSubject.
6.2 需求引入
每个城镇都需要配对服务,不是吗?你负责帮对象村实现约会服务系统。你有一个好点子,就是在服务中加入“Hot”和“Not”的评鉴,“Hot”就表示喜欢对方,“Not”表示不喜欢。你希望这套系统能鼓励你的顾客找到可能的配对对象,这也会让事情更有趣。
这个服务系统涉及到一个Person bean,允许设置或者取得一个人的信息。
首先实现一个Person接口:
public interface Person {
String getName();
String getGender();
String getInterests();
int getGeekRating();//评分为1-10
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
void setGeekRating(int rating);
}
然后实现PersonBean:
package javaproxy;
public class PersonImpl implements Person {
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
@Override
public String getName() {
return name;
}
@Override
public String getGender() {
return gender;
}
@Override
public String getInterests() {
return interests;
}
@Override
public int getGeekRating() {
if (ratingCount == 0) {
return 0;
}
return (rating/ratingCount);
}
@Override
public void setName(String name) {
this.name = name;
}
@Override
public void setGender(String gender) {
this.gender = gender;
}
@Override
public void setInterests(String interests) {
this.interests = interests;
}
@Override
public void setGeekRating(int rating) {
this.rating += rating;
ratingCount++;
}
}
如果直接通过该类的话,那么任意客户都可以篡改别人的数据。因为,根据我们定义PersonBean的方式,任何客户都可以调用任何方法。
这是一个我们可以使用保护代理的绝佳例子。什么是保护代理?这是一种根据访问权限决定客户可否访问对象的代理。比方说,如果你有一个雇员对象,保护代理允许雇员调用对象上的某些方法,经理还可以多调用一些其他的方法(像setSalary()) ,而人力资源处的雇员可以调用对象上的所有方法。
需求:在我们的约会服务中,我们希望顾客可以设置自己的信息,同时又防止他人更改这些信息。HotOrNot评分则相反,你不能更改自己的评分,但是他人可以设置你的评分。我们在PersonBean中已经有许多getter方法了,每个方法的返回信息都是公开的,任何顾客都可以调用它们。
6.3 明确需求
顾客不可以改变自己的HotOrNot评分,也不可以改变其他顾客的个人信息。要修正这些问题,必须要创建两个代理:一个访问你自己的PersonBean对象,另一个访问另一个PersonBean对象。这样,代理就可以控制在每一种情况下允许哪一种请求。
创建这种代理,我们必须使用Java API的动态代理。Java会为我们创建两个代理,我们只需要提供handler来处理代理转来的方法。
步骤一:创建两个InvocationHandler
InvocationHandler实现了代理的行为,Java负责创建真实代理类和对象。我们只需提供在方法调用发生时知道做什么的handler.
步骤二:写代码创建动态代理
步骤三:利用适当的代理包装任何PersonBean对象
当我们需要使用PersonBean对象时,如果不是顾客自己(称为“拥有者”),就是另一个顾客正在检查的服务使用者(称为“非拥有者”)。
不管是哪种情况,我们都为PersonBean创建合适的代理。
6.4? ?步骤1:创建InvocationHandler
我们知道需要编写两个InvocationHandler(调用处理器),其中一个给拥有者使用,另一个非拥有者。
什么是InvocationHandler?
当代理的方法被调用时,代理就会把这个调用转发给InvocationHnadler,但是这并不是通过调用InvocationHandler的相应方法做到的。那么是如何做到的?让我们来看看InvocationHandler的接口:
这里只有一个名为Invoke()的方法,不管代理被调用的是何种方法,处理器被调用的一定是invoke()方法。让我们看看这是如何工作的:
?
?当proxy调用invoke()时,要如何应对?通常,会先检查该方法是否来自proxy,并基于该方法的名称和变量做决定。现在来实现OwnerInvocationHandler,来了解工作机制:
这相当于是一个固定的调用逻辑。
package javaproxy;
import java.lang.reflect.*;
//拥有者的类,允许set&get自己的属性
//顾客可以设置自己的信息,同时又防止他人更改这些信息。
// HotOrNot评分则相反,你不能更改自己的评分,但是他人可以设置你的评分
public class OwnerInvocationHandler implements InvocationHandler {
Person person;
public OwnerInvocationHandler(Person person) {
//将person传入构造器,并保持它的引用
this.person = person;
}
//每次proxy被调用,就会导致proxy调用此方法
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException {
try {
if (method.getName().startsWith("get")) {
//如果方法是getter,作为拥有者,允许获取自己的属性
return method.invoke(person, args);
} else if (method.getName().equals("setGeekRating")) {
//如果方法是setGeekingRating(),就抛出非法异常表示不允许
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {
//如果方法是setter,作为拥有者,允许修改自己的属性
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
//如果调用其他方法,一律不理,返回null
return null;
}
}
6.5 步骤2:创建Proxy类并实例化Proxy对象
现在,只剩下创建动态Proxy类,并实例化Proxy对象了。让我们开始编写一个以PersonBean为参数,并知道如何为PersonBean对象创建拥有者代理的方法。也就是说,我们要创建一个代理,将它的方法调用转发给OwnerInvocationHandler。代码如下:
//这个方法需要传入一个person对象作为参数,然后返回它的代理
//因为代理和主体有相同的接口,所以返回一个Person
Person getOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),//获取类加载器
person.getClass().getInterfaces(),//获取接口
new OwnerInvocationHandler(person));//调用处理器
}
测试服务:
package javaproxy;
import java.lang.reflect.*;
import java.util.*;
public class MatchMakingTestDrive {
//模拟数据库
HashMap<String, Person> datingDB = new HashMap<String, Person>();
public static void main(String[] args) {
MatchMakingTestDrive test = new MatchMakingTestDrive();
test.drive();
}
public MatchMakingTestDrive() {
initializeDatabase();
}
public void drive() {
Person joe = getPersonFromDatabase("Joe Javabean"); //从数据库中取出一个人的信息
Person ownerProxy = getOwnerProxy(joe);//创建拥有者代理 (与RealSubject都引用了Person接口)
System.out.println("Name is " + ownerProxy.getName());//调用getter
ownerProxy.setInterests("bowling, Go");//调用setter
System.out.println("Interests set from owner proxy");
try {
ownerProxy.setGeekRating(10);//调用setter,直接有Proxy来调用的
//但是其实还是由proxy委托OwnerInvoacitonHandler来执行
} catch (Exception e) {
System.out.println("Can't set rating from owner proxy");
}
System.out.println("Rating is " + ownerProxy.getGeekRating());
//非拥有者类的代理也是同样的
Person nonOwnerProxy = getNonOwnerProxy(joe);
System.out.println("Name is " + nonOwnerProxy.getName());
try {
nonOwnerProxy.setInterests("bowling, Go");
} catch (Exception e) {
System.out.println("Can't set interests from non owner proxy");
}
nonOwnerProxy.setGeekRating(3);
System.out.println("Rating set from non owner proxy");
System.out.println("Rating is " + nonOwnerProxy.getGeekRating());
}
//这个方法需要传入一个person对象作为参数,然后返回它的代理
//因为代理和主体有相同的接口,所以返回一个Person
Person getOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),//获取类加载器
person.getClass().getInterfaces(),//获取接口
new OwnerInvocationHandler(person));//调用处理器
}
Person getNonOwnerProxy(Person person) {
return (Person) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new NonOwnerInvocationHandler(person));
}
Person getPersonFromDatabase(String name) {
return (Person)datingDB.get(name);
}
//模拟从数据库中获取初始数据
void initializeDatabase() {
Person joe = new PersonImpl();
joe.setName("Joe Javabean");
joe.setInterests("cars, computers, music");
joe.setGeekRating(7);
datingDB.put(joe.getName(), joe);
Person kelly = new PersonImpl();
kelly.setName("Kelly Klosure");
kelly.setInterests("ebay, movies, music");
kelly.setGeekRating(6);
datingDB.put(kelly.getName(), kelly);
}
}
输出:
6.6? 总结
问1:到底“动态代理”动态在哪里?
回1:不是。动态代理之所以被称为动态,是因为运行时才将它的类创建出来。代码开始执行时,还没有proxy类,它是根据需要从你传入的接口集创建的。
问2:InvocationHandler看起来像一个奇怪的proxy。它没有实现所代理的类的任何方法
回2:因为InvocationHandler根本就不是proxy,他只是一个帮助proxy的类,proxy会把调用转发给它处理。Proxy本身是利用静态的Proxy.newProxyInstance()方法在运行时动态的创建的。
问3:有没有办法知道某个类是不是代理类?
回3:代理类有个静态方法,叫做isProxyClass()。此方法返回值为true,就是一个动态代理类。
7.其他常见代理
1.防火墙代理
2.智能引用代理
3.缓存代理
?
4.同步代理
5.复杂式隐藏代理
6.写时复制代理
?
8.总结
- 代理模式为另一个对象提供代表,以便控制客户对对象的访问,管理访问的方式有许多种。
- 远程代理管理客户和远程对象之间的交互。
- 虚拟代理控制访问实例化开销大的对象。
- 保护代理基于调用者控制对对象方法的访问。
- 代理模式有许多变体﹐例如:缓存代理、同步代理、
- 防火墙代理和写入时复制代理。
- 代理在结构上类似装饰者,但是目的不同。
- 装饰者模式为对象加上行为,而代理则是控制访问。Java内置的代理支持,可以根据需要建立动态代理,并将所有调用分配到所选的处理器。
- 就和其他的包装者(wrapp-er)一样,代理会造成你的设计中类的数目增加。
?
?
|