控制和管理访问
玩过扮白脸,扮黑脸的游戏吗?你是一个白脸,提供很好且很友善的服务,但是你不希望每个人都叫你做事,所以找了黑脸控制对你的访问。这就是代理要做的:控制和管理对象。
监视器编码
需求:糖果机能够获得更好的监控,需要提供一份库存以及机器状态的报告。
先为GumballMachine加上处理位置的支持:
public class GumballMachine{
//位置用String记录
String location;
//位置被传入构造器中,然后存到实例变量中
public GumballMachine(String location,int count){
//构造器内的其他代码
this.location = location;
}
//getter方法,获取位置信息
public String getLocation(){
return location;
}
}
创建另一个类,GumballMonitor(糖果监视器),以便取得机器的位置、糖果的库存量以及当前机器的状态,并打印成一份报告。
public class GumballMonitor{
GumballMachine 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());
}
}
远程代理
远程代理就好比“远程对象的本地代表”。
何谓“远程对象”?这是一种对象,活在不同的java虚拟机(JVM)堆中。可以理解为在不同的地址空间运行的远程对象。
何谓“本地代表”?这是一种可以由本地方法调用的对象,其行为会转发到远程对象中。
你的客户对象所做的就像是在做远程方法调用,但其实只是调用本地堆中的“代理”对象上的方法,再由代理处理所有网络通信的底层细节。
远程代理应用
如何创建一个代理,知道如何调用在另一个JVM中的对象的方法?
我们无法取得另一个堆的对象的引用,不能这样写:
Duck d = <另一个堆的对象>
RMI可以让我们找到远程JVM内的对象,并允许我们调用它们的方法。
关于RMI调用和本地(正常)的方法调用,有一个不同点。虽然调用远程方法就如同调用本地方法一样,但是客户辅助对象会通过网络发送方法调用,所以网络和I/O的确是存在的。
定义代理模式
远程代理是一般代理模式的一种实现,这个模式的变体很多。
代理模式为另一个对象提供一个替身或占位符以控制对这个对象的访问。
使用代理模式创建代表(representative)对象,让代表对象控制某对象的访问,被代理的对象可以是远程的对象、创建开销大的对象或需要安全控制的对象。
在糖果机的例子中,代理控制了对远程对象的访问。代理之所以需要控制访问,是因为我们的客户(监视器)不知道如何和远程对象沟通。从某个方面来看,远程代理控制访问,好帮我们处理网络上的细节。
代理模式有许多变体,而这些变体几乎都和“控制访问”的做法有关:
- 远程代理控制访问远程对象
- 虚拟代理控制访问创建开销大的资源
- 保护代理基于权限控制对资源的访问
代理模式总结
代理模式与装饰者模式的区别是什么?
装饰者模式为对象增加行为,而代理是控制对象的访问
代理模式与适配器模式的区别是什么?
代理和适配器都是挡在其他对象的前面,并负责将请求转发给它们。适配器会改变对象适配的接口,而代理则实现相同的接口
如何让客户使用代理,而不是真正的对象?
常用的技巧是提供一个工厂,实例化并返回主题。因为这是在工厂方法内发生的,我们可以用代理包装主题再返回,而客户不知道也不在乎他使用的是代理还是真东西
保护代理应用
Java在java.lang.reflect包中由自己的代理支持,利用这个包你可以在运行时动态地创建一个代理类,实现一个或多个接口,并将方法的调用转发到你所指定的类。
因为实际的代理类是在运行时创建的,我们称这个java技术为:动态代理
我们要利用Java的动态代理创建我们下一个代理实现(保护代理)。但在这之前,先让我们看一下类图,了解下动态代理是怎么一回事,就和真实世界中大多数的事物一样,它和代理模式的传统定义有一点出入。
上图所示,因为java已经为你创建了Proxy类,所以你需要有办法来告诉Proxy类你要做什么。你不能像以前一样把代码放在Proxy类中,因为Proxy不是你直接实现的。既然这样的代码不能放在Proxy类中,那么要放在哪里?放在InvocationHandler中。InvacationHandler的工作是响应代理的任何调用。你可以把InvacationHandler想成是代理收到方法调用后,请求做实际工作的对象。
对象村的配对
对乡村实现约会服务系统,每个城镇都配对服务,服务系统涉及一个Person bean,允许设置或取得一个人的信息:
public interface PersonBean{
String getName();
String getGender();
String getInterests();
int getHotOrNotRating();
void setName(String name);
void setGender(String gender);
void setInterests(String interests);
//需要一个整数作为参数,并将它加入此人的运行平均值中
void setHotOrNotRating(int rating);
}
public class PersonBeanImpl implements PersonBean{
String name;
String gender;
String interests;
int rating;
int ratingCount = 0;
public String getName(){
return name;
}
public String getGender(){
return gender;
}
public int getHotOrNotRating(){
if(ratingCount == 0){
return 0;
}
return rating/ratingCount;
}
public void setName(String name){
this.name = name;
}
public void setGender(String gender){
this.gender = gender;
}
public void setInterests(String interests){
this.interests = interests;
}
public void setHotOrNotRating(int rating){
this.rating+=rating;
ratingCoun++;
}
}
系统不应该允许用户篡改别人的数据,根据我们定义PersonBean的方式,任何客户都可以调用任何方法。
这是一个我们可以使用保护代理的绝佳例子。什么事保护代理?这是一种根据访问权限决定客户可否访问对象的代理。比方说,如果你有一个雇员对象:
- 保护代理允许雇员调用对象上的某些方法
- 保护代理允许经理可以多调用一些其他的方法,像setSalary()
- 保护代理允许人力资源雇员调用对象上的所有方法
我们希望顾客可以设置自己的信息,同时又防止他人更改这些信息。HotOrNot评分则相反,你不能更改自己的评分,但是他人可以设置你的评分。
为PersonBean创建动态代理
顾客不可以改变自己的HotOrNot评分,也不可以改变其他顾客的个人信息。要修正这些问题,必须创建两个代理:
- 访问自己PersonBean对象
- 访问另一个顾客的PersonBean对象
步骤一:创建两个InvocationHandler
InvocationHandler实现了代理的行为,java负责创建真实代理类和对象。我们只需提供在方法调用发生时知道做什么的handler
这里只有一个名为invoke()的方法,不管代理被调用的是何种方法,处理器被调用的一定是invoke()方法。
- 假设proxy的setHotOrNotRating()方法被调用
proxy.setHotOrNotRating(9);
- proxy会接着调用invocationHandler的invoke()方法
//Method类是Reflection API的一部分,利用它的getName()方法,我们就可以知道proxy被调用的方法是什么
invoke(Object proxy,Method method,Object[] args)
- handler决定要如何处置这个请求,转发给RealSubject
//我们调用原始proxy被调用的方法。这个对象在调用时被传给我们,只不过加载调用的是真正的主题(person)
return method.invoke(person,args);
当proxy调用invoke()时,要如何应对?通常,会先检查该方法是否来自proxy,并基于该方法的名称和变量做决定。现在我们就来实现OwnerInvocationHandler,以了解工作机制:
import java.lang.reflect.*;
//所有调用处理器都实现InvocationHandler接口
public class OwnerInvocationHandler implements InvocationHandler{
PersonBean person;
//我们将person传入构造器,并保持它的引用
public OwnerInvocationHandler(PersonBean person){
this.person = person;
}
//每次proxy的方法被调用,就会导致proxy调用此方法
public Object invoke(Object proxy,Method method,Object[] args) throws IllegalAccessException{
try{
if(method.getName().startsWith("get")){
//如果方法是一个getter,我们就调用person内的方法
return method.invoke(person,args);
}else if(method.getName.equals("setHotOrNotRating")){
//如果是setHotOrNotRating()方法,我们就抛出IllegalAccessException表示不允许
throw new IllegalAccessException();
}else if(method.getName().startsWith("set")){
//因为我们是拥有者,所以任何其他set方法都可以,我们就在真正主题上调用它
return method.invoke(person,args);
}
}catch(InvocationTargetException e){
//真正主题抛出异常的话,就会执行这里
e.printStackTrace();
}
return null;
}
}
步骤二:创建动态代理
创建动态Proxy类,并实例化Proxy对象。我们编写一个以PersonBean为参数,并知道如何为PersonBean对象创建拥有者代理的方法。也就是说,我们要创建一个代理,将它的方法调用转发给OwnerInvocationHandler。代码如下:
//此方法需要一个person对象作为参数,然后返回它的代理,因为代理和主题有相同的接口,所以我们返回一个PersonBean
PersonBean getOwnerProxy(PersonBean person){
//利用Proxy类的静态newProxyInstance方法创建代理
return (PersonBean)Proxy.newProxyInstance(person.getClass().getClassLoader(),
//代理需要实现的接口
person.getClass.getInterfaces(),
new OwnerInvocationHandler(person));
}
步骤三:利用适当的代理包装任何PersonBean对象
来看下代理如何控制对setter方法的访问
public class MatchMakingTestDrive{
public MatchMakingTestDrive(){
initializeDatabase();
}
public void drive(){
//从数据库中取出一个人
PersonBean joe = getPersonFromDatabase("Joe");
PersonBean ownerProxy = getOwnerProxy(joe);
ownerProxy.setInterests("bowling,Go");
try{
ownerProxy.setHotOrNotRating(10);
}catch(Exception e){
System.out.println("Can't set rating from owner proxy");
}
PersonBean nonOwnerProxy = getNonOwnerProxy(joe);
//非拥有者代理
nonOwnerProxy.setHotOrNotRating(10);
try{
nonOwnerProxy.setInterests("bowling,Go");
}catch(Exception e){
System.out.println("Can't set interests from non owner proxy");
}
}
}
总结
- “动态代理”动态在哪里?是不是指在运行时才将它实例化并和handler联系起来?
不是的。动态代理之所以被称为动态,是因为运行时才将它的类创建出来。代码开始执行时,还没有proxy类,它是根据需要从你传入的接口集创建的。
- InvocationHandler看起来像一个很奇怪的proxy。它没有实现所代理的类的任何方法。
这是因为InvocationHandler根本就不是proxy,它只是一个帮助proxy的类,proxy会把调用转发给它处理。Proxy本身是利用静态的Proxy.newProxyInstance()方法在运行时动态地创建的。
- 有没有办法知道某个类是不是代理类呢?
可以。代理类有一个静态方法,叫做isProxyClass()。此方法的返回值如果为true,表示这是一个动态代理类。除此之外,代理类还会实现特定的某些接口。
- 代理模式
- 包装另一个对象,并控制对它的访问
- 适配器模式
- 包装另一个对象,并提供不同的接口
- 装饰者模式
- 包装另一个对象,并提供额外的行为
- 外观模式
- 包装许多对象以简化它们的接口