【HeadFirst 设计模式学习笔记】12 代理模式
作者:gnuhpc
出处:http://www.cnblogs.com/gnuhpc/
1.这一节的任务是我们需要完成对上一节的糖果机产生一个机器状况和余量的报告,若这个报告在本地(不是通过Internet)生成的话,那么我们的设计就很简单了,在糖果机中加入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());
}
}
2.但是,我们的需求进一步加强——要求有一个远端可以获得这个报告,这有点RMI的意思(具体什么叫做RMI,有什么特点请看这篇文字)那么我们就需要一个叫做远程代理的东西了。所谓代理(proxy),就是代表一个真实的对象,在这个例子中,代理就像是糖果机一样,但其实幕后是它利用网络和一个远程的真正糖果机进行沟通。基本的想法如下图:
在这个场景中,客户对象所做的就像是直接操作远端一样,但是实际上它只是操作了本地堆上的代理对象的方法,而代理则帮忙处理了所有网络的通信。
1) 制作远程接口:
public interface GumballMachineRemote extends Remote {//这个是对RMI有规定意义的,请遵循。
public int getCount() throws RemoteException;//声明的所有方法都要抛出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();
}
在上一节中我们实现的各个状态内部都有一个糖果机的引用,而使用这个引用可以直接改变糖果机所在的状态,我们不希望这样的能力通过序列化传到对端,也就是说,类中的部分内容我们希望在序列化的时候忽略掉,那么我们可以使用transient关键字告诉JVM不要序列化这个字段。
2)制作远程的实现:
这是在远端实际干活的家伙,它首先要继承UnicastRemoteObject类,以成为远程服务,其次它还要实现GumballMachineRemote 这个远程接口的实际功能。
public class GumballMachine
extends UnicastRemoteObject implements GumballMachineRemote
{
State soldOutState;
State noQuarterState;
State hasQuarterState;
State soldState;
State winnerState;
State state = soldOutState;
int count = 0;
String location;
public GumballMachine(String location, int numberGumballs) throws RemoteException {//设计一个构造方法来抛出异常,这是因为UnicastRemoteObject 就抛出异常,超类抛出异常时,子类也只得抛出同样的异常。
soldOutState = new SoldOutState(this);
noQuarterState = new NoQuarterState(this);
hasQuarterState = new HasQuarterState(this);
soldState = new SoldState(this);
winnerState = new WinnerState(this);
this.count = numberGumballs;
if (numberGumballs > 0) {
state = noQuarterState;
}
this.location = location;
}
public void insertQuarter() {
state.insertQuarter();
}
public void ejectQuarter() {
state.ejectQuarter();
}
public void turnCrank() {
state.turnCrank();
state.dispense();
}
void setState(State state) {
this.state = state;
}
void releaseBall() {
System.out.println("A gumball comes rolling out the slot...");
if (count != 0) {
count = count - 1;
}
}
public void refill(int count) {
this.count = count;
state = noQuarterState;
}
public int getCount() {
return count;
}
public State getState() {
return state;
}
public String getLocation() {
return location;
}
public State getSoldOutState() {
return soldOutState;
}
public State getNoQuarterState() {
return noQuarterState;
}
public State getHasQuarterState() {
return hasQuarterState;
}
public State getSoldState() {
return soldState;
}
public State getWinnerState() {
return winnerState;
}
public String toString() {
StringBuffer result = new StringBuffer();
result.append("/nMighty Gumball, Inc.");
result.append("/nJava-enabled Standing Gumball Model #2004");
result.append("/nInventory: " + count + " gumball");
if (count != 1) {
result.append("s");
}
result.append("/n");
result.append("Machine is " + state + "/n");
return result.toString();
}
}
3)我们可以把Server端的服务启动起来了,通过一个程序绑定一下:
public class GumballMachineTestDrive {
public static void main(String[] args) {
GumballMachineRemote gumballMachine = null;
int count;
if (args.length < 2) {
System.out.println("GumballMachine ");
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();
}
}
}
首先我们在命令行中启动rmiregistry,然后运行这个类,Server端就开始工作了。
4)我们开始编写客户端
我们对原有的客户端不需要太大的改动,
public class GumballMonitor {
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();
}
}
}
5)我们写一个监控系统,监控多个糖果机的状态,这时我们就彻底完成了该系统的设计和实现:
public class GumballMonitorTestDrive {
public static void main(String[] args) {
String[] location = {"rmi://santafe.mightygumball.com/gumballmachine",
"rmi://boulder.mightygumball.com/gumballmachine",
"rmi://seattle.mightygumball.com/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();
}
}
}
6)举这个例子的目的其实是想阐明代理模式(远程代理模式只是一个特例),这个模式为另一个对象提供一个替身或者占位符以控制对这个对象的访问。主要的代理模式有三种,我们会在下边的部分介绍另两个:
- 远程代理控制访问远程对象
- 虚拟代理控制访问创建开销大的资源
- 保护代理基于权限控制对资源的访问
为了让程序真正使用代理而不是直接使用相关的类,一个常用的技巧是提供一个工厂。适配器模式和代理模式都是挡在其他对象前边的方式,但是前者会改变对象适配的接口,而代理模式则实现相同的接口。
7)我们下边介绍一下虚拟代理:
它作为创建开销大的对象的代表,当我们真正需要创建一个对象时才创建它,当对象在创建前和创建中时,虚拟代理来扮演对象的替身,对象创建后,代理会将请求直接委托给对象。
我们设定这样一个例子,一个播放软件要从线上得到唱片的封套,这个动作是很耗时的,那么我们可以设置一个Proxy,当图片没有加载的时候就在界面上显示“正在加载”,而当图片加载完毕以后(在这个时间点,真正绘图的类被实例化),Proxy把所有关于图案显示的方法都委托给真正的显示控制类。在以后这个真正绘图的类实例创建以后,下次再调用绘图代理就直接委托它来完成了。下边我们举一个例子来说明(非原书例子),我们假设一个程序要使用Email服务,但是并不是什么时候都要去接入Email的,而Email的接入是非常耗时耗资源的一件事情,那么这个场景就很合适于这个模式。
首先,我们要先设定一个公共接口,这个接口为代理和实际的实现提供了相同的方法定义:
public interface EMailService {
public void sendMail(String receiver, String subject, String text);
public void receiveMail(String receiver);
}
接着,我们实际来实现一个类,这个类实现了上边的接口,这个类需要的就是适时的载入,只要当需要的时候再实例化这个类。当然在这里我们只是做示意性质的实现:
public class RealEMailService implements EMailService{
@Override
public void sendMail(String receiver, String subject, String text) {
System.out.println("Sending mail to '" + receiver + "'" + " with subject '" +
subject + "'" + " and message '" + text + "'");
}
@Override
public void receiveMail(String receiver) {
System.out.println("Receiving mail from '" + receiver + "'");
}
}
再接着,我们为上边的这个类创建一个代理类,这个代理类必须遵循接口的方法定义。在许多情况下,Client甚至不知道他们是在Proxy上进行方法的调用的。在代理类中,要持有一个真实实现的类的引用,以便在合适的时候将事情移交给实际的类实现。
public class ProxyEMailService implements EMailService{
private RealEMailService emailService;
@Override
public void receiveMail(String receiver) {
if (emailService == null){
emailService = new RealEMailService();
}
emailService.receiveMail(receiver);
}
@Override
public void sendMail(String receiver, String subject, String text) {
if (emailService == null){
emailService = new RealEMailService();
}
emailService.sendMail(receiver, subject, text);
}
}
我们就利用上述两个类完成一个程序的构建,在程序中,我们创建一个指向Proxy的引用,虽然它被声明为共同的接口:
public class Application {
public EMailService locateEMailService(){
EMailService emailService = new ProxyEMailService();//时时刻刻都透着面向接口编程的原则
return emailService;
}
}
这样,我们就完成了这个程序的设计,下边我们来测试一下:
public class ApplicationClient { public static void main(String[] args) { Application application = new Application(); EMailService emailService = application.locateEMailService();//注意,此时我们并没有真正创建EMailService的实例
emailService.sendMail("abc@gmail.com", "Hello", "A test mail");//当我们实际使用时我们才创建,这样就达到了虚拟代理的目的了。 emailService.receiveMail("abc@gmail.com"); } }
8)现在介绍保护代理Protection Proxy
Java中有一个叫做动态代理的技术,在java.lang.reflect包中,这个机制和我们上边的有一点点差别:
此时Java为你创建了Proxy类,但是你没有想以前那样将控制代码放入Proxy中,此时我们需要将这些代码放到一个实现了InvocationHandler接口的InvocationHandler类中去。在介绍保护代理时,我们就打算用这个技术来做具体的实现。
在具体介绍保护代理前,我们现在假设一个场景:
有一个系统,我们需要对每一个用户维护一套操作(比如setName、setGender等等,需要注意的是这个例子中的setHotOrNotRating则正好相反,这是别人对你的评分,你自己无权评分),每一个人都能修改自己的属性,而不能修改对方的属性,那么这样的接入保护是如何做到的呢?
首先,我们先定义修改的相关动作作为共同的方法放入一个接口之中:
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 String getInterests() {
return interests;
}
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;
ratingCount++;
}
}
下一步请注意,这里就有所区别了。再次提醒,现在我们是在使用Java内置的Proxy代理模式——我们要创建两个InvocationHandler,InvocationHandler完成了Proxy的行为,而具体创建实际的Proxy对象则是Java 自己替我们去做的事情了,我们仅仅需要提供一个Handler以便在一个方法被调用时该做些什么。你可以把InvocationHandler想象为:当代理的方法被调用时,代理就会把调用发给InvocationHandler,需要注意的是,此时代理不管调用什么方法都并不是直接调用InvocationHandler的方法,而是调用了它自身的invoke方法来处理。
我们首先设计拥有者:
public class OwnerInvocationHandler implements InvocationHandler {
PersonBean person;//我们保持一个引用在每一个InvocationHandler中
public OwnerInvocationHandler(PersonBean person) {
this.person = person;
}
public Object invoke(Object proxy, Method method, Object[] args) //每一次Proxy的方法被调用就会导致Proxy调用这个方法
throws IllegalAccessException {
try {
if (method.getName().startsWith("get")) {//若一个方法是一个getter,那么不需要对其进行访问控制,所以就直接调用
return method.invoke(person, args);
} else if (method.getName().equals("setHotOrNotRating")) {//这个为什么请看上边的场景说明
throw new IllegalAccessException();
} else if (method.getName().startsWith("set")) {//若是setter,因为是owner,就直接调用
return method.invoke(person, args);
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
接着,我们设计非拥有者:
public class NonOwnerInvocationHandler implements InvocationHandler {
PersonBean person;
public NonOwnerInvocationHandler(PersonBean person) {
this.person = person;
}
public Object invoke(Object proxy, Method method, Object[] args)
throws IllegalAccessException {
try {
if (method.getName().startsWith("get")) {
return method.invoke(person, args);
} else if (method.getName().equals("setHotOrNotRating")) {
return method.invoke(person, args);//与Owner相比,此处的处理与下边的setter换一下
} else if (method.getName().startsWith("set")) {
throw new IllegalAccessException();
}
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
}
好了,我们现在可以真正创建一个Proxy并且测试一下我们的程序,使用Java自带的Proxy类中的 newProxyInstance
静态方法创建代理,这个方法如下:
static Object
newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h)
第一个参数是传入类的加载器,第二个参数是传入类的接口,第三个是告诉它该使用什么样的InvocationHandler进行处理。
public class MatchMakingTestDrive {
Hashtable datingDB = new Hashtable();
public static void main(String[] args) {
MatchMakingTestDrive test = new MatchMakingTestDrive();
test.drive();
}
public MatchMakingTestDrive() {
initializeDatabase();//初始化一些数据
}
public void drive() {//开始运行
PersonBean joe = getPersonFromDatabase("Joe Javabean");
PersonBean ownerProxy = getOwnerProxy(joe);
System.out.println("Name is " + ownerProxy.getName());
ownerProxy.setInterests("bowling, Go");
System.out.println("Interests set from owner proxy");
try {
ownerProxy.setHotOrNotRating(10);
} catch (Exception e) {
System.out.println("Can't set rating from owner proxy");
}
System.out.println("Rating is " + ownerProxy.getHotOrNotRating());
PersonBean 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.setHotOrNotRating(3);
System.out.println("Rating set from non owner proxy");
System.out.println("Rating is " + nonOwnerProxy.getHotOrNotRating());
}
PersonBean getOwnerProxy(PersonBean person) {//设置Owner代理
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),
new OwnerInvocationHandler(person));
}
PersonBean getNonOwnerProxy(PersonBean person) {//设置非Owner代理
return (PersonBean) Proxy.newProxyInstance(
person.getClass().getClassLoader(),
person.getClass().getInterfaces(),//
new NonOwnerInvocationHandler(person));//设置处理的方式
}
PersonBean getPersonFromDatabase(String name) {
return (PersonBean)datingDB.get(name);
}
void initializeDatabase() {//首先我们要有一些数据进行操作
PersonBean joe = new PersonBeanImpl();
joe.setName("Joe Javabean");
joe.setInterests("cars, computers, music");
joe.setHotOrNotRating(7);
datingDB.put(joe.getName(), joe);
PersonBean kelly = new PersonBeanImpl();
kelly.setName("Kelly Klosure");
kelly.setInterests("ebay, movies, music");
kelly.setHotOrNotRating(6);
datingDB.put(kelly.getName(), kelly);
}
}
这样我们就完成了保护代理的任务,代码在执行时还没有Proxy类,它是根据需要从传入的接口中创建的。