设计形式之略见一斑(代理模式Proxy)

设计模式之略见一斑(代理模式Proxy)

普通对象所需要完成的任务就是通过公共接口为外界提供自己所承诺的服务。然而,有时候合法的对象可能会因为各种原因而无法完成自己常规的任务。尤其是当对象需要长时候才能载入内存、对象正运行在另一台计算机上或者需要获取对象消息的时候,这种情况会频繁出现。在这种情况下,我们可以使用一个代理对象,由它来承担客户期待的责任,并将请求转发给其背后的目标对象。

 

Proxy模式的意图在于为对象提供一个代理或者占位来控制该对象的访问.

 

下面对代理对象的各种情况进行一一分析,它分别有

图像代理,远程代理,动态代理

 

1)图像代理

    代理通常拥有与实际对象基本相同的接口。代理的工作方式是:把服务请求明智地转发给代理控制的底层对象,最终完成任务。避免将较大的图像加载到内存就是一个典型的例子。假定我们要加载一张较大的图像,我们需要为这张图像建立代理,通过先加载一张显示loading的小图片,然后当大图像已完全加载到内存时,代理通知替换这张loading小图片。

 

代码如下:

public class ImageProxy implements Runnable{
	static final ImageIcon BLACK = new ImageIcon("images/black.jpg");
     static final ImageIcon LOADING = new ImageIcon("images/loading.jpg");
     protected String filename;
     protected JFrame jf;
     ImageIcon currentImage = BLACK;
     public ImageProxy(String filename){
    	 this.filename = filename;
     }
     public void load(JFrame jf){
    	 this.jf = jf;
    	 currentImage = LOADING;
    	 jf.repaint();
    	 new Thread(this).start();
     }
     public void run(){
    	 currentImage = new ImageIcon(ClassLoader.getSystemResource(filename));
    	 jf.pack();
     }
     ..
     ..
}

 代理类ImageProxy通过线程方式,当大图片加载完成时就替换Loading状态的图片

 

 

2)远程代理

 

   如果我们期望调用正在另一台计算机上运行的对象的方法,那么必须找到一种方法来与该远程对象进行通信,而不能直接调用其方法。我们可以在远程机器上打开一个套接字,并设计一种协议用于向该远程对象发送消息。理想情况下,这种方案可以让我们*地与远程对象进行通信,就像与本地对象进行通信一样。在这种方案下使用代理模式,可直接调用位于本地的代理对象的方法,该代理对象将调用请求发给远程对象。实际上,著名的公共对象请求代理架构(CORBA),ASP.NET以及JAVA 的远程方法调用(RMI)已经实现了这种方案。

   在RMI中代理对象用于将调用请求转发给另一台计算机上运行的指定对象,客户可以很容易地获得这种代理对象。企业JavaBeans(EJB)规范是业务新兴的一个重要标准,它的基础之一便是RMI;要理解EJB,必须先了解RMI。无论业界标准如何发展,我们可以预见未来的分布计算仍将离不开代理模式。RMI为代理模式的实践提供了一个很好的例子。

   下面我们简单介绍一下它的实现方式:

 

首先我们的接口必须要继承Remote接口才能够被远程接收的到同时还要扩展UnicastRemoteObject类。

我们的接口定义如下:

 

/**
 * 远程代理接口
 * @author Administrator
 *
 */
public interface ProxyRemote extends Remote {
	public String getName() throws RemoteException;
}

 

 

接口实现如下:

 

/**
 * 远程代理实现
 * @author Administrator
 *
 */
public class RemoteImpl extends UnicastRemoteObject implements ProxyRemote{
	private String name;
	public RemoteImpl(String name) throws RemoteException{
		this.name = name;
	}
	public String getName() {
		return "remote name is:"+name;
	}	
}

 

RemoteImpl 运行在一台机器上,为了能够让其它机子访问它,我们需要为RemoteImpl 对象提供一个代理对象。这个代理对象必须实现 Remote接口,并且提供用于与远程对象通信的附加特性。RMI的最大便利之一就是它能够自动的创建这个代理类。为了自动生成代理类,我们必须把RemoteImpl.java 与ProxyRemote.java 接口文件放在RMI注册的运行目录下.


设计形式之略见一斑(代理模式Proxy)

 >javac packagename.RemoteImpl.java

 >rmic packagename.RemoteImpl                         //RMI编译器,生成用于简化通信的存根

 

rmic命令编译了它所需要的所有类,并创建 了RemoteImpl_Stub类。

 

在对象能够被访问之前,我们必须运行在服务器的上的RIM注册程序注册该对象。

>rmiregistry 5000

 

在端口5000进行监听,注册完之后我们可以开始创建注册RemoteImpl对象.

/**
 * 注册远程代理
 * @author Administrator
 *
 */
public class RegisterRemote {
	public static void main(String[] args) {
		try {
			RemoteImpl remoteImpl = new RemoteImpl("it's a good test!");
			Naming.rebind("rmi://localhost:5000/Remote", remoteImpl);
			System.out.println("register remoteImpl ok");
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}

 

对象被注册完成之后,我们可以通过其他机子访问并得到这个对象,这里本地我们就用localhost来访问并获得这个对象

/**
 * 接收远程代理 
 * @author Administrator
 *
 */
public class ShowRegisterClient {
	public static void main(String[] args) {
		Object obj;
		try {
			obj = java.rmi.Naming.lookup("rmi://localhost:5000/Remote");
			ProxyRemote proxyRemote = (ProxyRemote)obj;
			System.out.println("the result is:"+proxyRemote.getName());
		} catch (MalformedURLException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (RemoteException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (NotBoundException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		
		
	}
}

 

当这个程序运行的时候,它首先会通过注册名Remote来查找远程对象。该远程对象代表着远程的一个RemoteImpl对象;lookup返回的对象是RemoteImpl_Stub类的一个实例,RemoteImpl_Stub实现了接口ProxyRemote接口,因此我们可以将这个强制转换为ProxyRemote接口的一个对象,从而可以调用它获得数据。

 

最后结果,屏幕上将会打印如下信息:

the result is:remote name is:it's a good test!

  

 

远程对象代理总结:

   RMI优点在于它使得客户端程序只需要与本地代理对象进行交互便可达到与远程对象通信的目的,RMI用户定义了客户端与服务器端共享对象的接口。RMI为客户端和服务器端分别提供一个ProxyRemote接口的实现类;这两个实现类相协作,从机时可完成进程间的无缝通信。服务器端和客户端则不必关心这些细节。

 

 

 

3)动态代理

 

    我们可能借助动态代理,使用代理对象来包装其他对象,使用代理对象截获对被包装对象的调用请求,然后代理继续把这些请求转发给被包装对象。在执行被截获的调用之前或者之后,我们可以编写相关的加强代码。

    动态代理需要使用对象的类所实现的接口,代理对象可以截获的调用就是这些接口定义的调用。也就是说如果某个类可以实现要截获的接口,动态代理就可以包装这个类的实例。  

  为了创建动态代理,必须具有要截获的的接口列表,如用如下代理就可以获取这个列表:

Class[] classes = obj.getClass().getInterfaces();

 

借助于上述代码,我们可以获得希望截获的方法列表,这些方法属于对象的类实现接口。为构建动态代理,还需要:类加载器,以及包含当代理捕获某些调用时你希望执行的行为的类。对于接口列表,通过使用与希望包装的对象相关的对象,就可以获合适的类加载器。

ClassLoader loader = obj.getClass().getClassLoader();

 

最后一个就是代理对象本身,这个对象必须是实现java.lang.reflect包中的InvocationHandler接口的类实例,接口声明如下:

 

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable ;

 

在动态代理中馐某对象时,对被包装对象的调用会转发给动态代理对象的invoke(),invoke()方法会继续把这个调用转发给被包装对象,可以使用如下进行转发调用:

 

result = m.invoke(obj,args);

 

 

 下面举出一个例子添加一些对象所需要的时候,如下在添加这个对象花费太多时间时就把这个对象打印出来.

public class BadPro {
	private String name;

	public BadPro(String name) {
		this.name = name;
	}

	@Override
	public int hashCode() {
		// TODO Auto-generated method stub
		try {
			Thread.sleep(20);
		} catch (Exception e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		return super.hashCode();
	}
	public String toString(){
		return name;
	}
}

 

 

      

public class GoodPro {
	private String name;

	public GoodPro(String name) {
		this.name = name;
	}

	@Override
	public int hashCode() {
		return super.hashCode();
	}
	public String toString(){
		return name;
	}
}

 

 

 

/**
 * 动态代理
 * @author Administrator
 *
 */
public class ImpatientProxy implements InvocationHandler {
	private Object obj;
	private ImpatientProxy(Object obj){
		this.obj = obj;
	}
	public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
		Object result;
		long t1 = System.currentTimeMillis();
		result = method.invoke(obj, args);
		long t2 = System.currentTimeMillis();
		if(t2-t1>10){
		  System.out.println(">It takes"+(t2-t1)+" millions to invoke with:"+method.getName());
		  for(int i =0;i<args.length;i++){
			  System.out.println("> arg["+i+"]"+"   name:"+args[i]+"   class name:"+args[i].getClass().getName());
		  }
		}
		return result;
	}
	public static Object newInstance(Object obj){
		ClassLoader loader = obj.getClass().getClassLoader();
		Class[] classes = obj.getClass().getInterfaces();
		return Proxy.newProxyInstance(loader, classes, 
				new ImpatientProxy(obj));
		
	}
	public static void main(String[] args) {
		Set s = new HashSet();
		s = (Set)ImpatientProxy.newInstance(s);
		s.add(new BadPro("BadPro"));
		s.add(new GoodPro("GoodPro"));
		s.add(new Object());
		System.out.println("the set contains :"+s.size()+" things.");
	}

}

 

这个类实现invoke方法,以检查被包装对象执行某调用操作所花费的时间太长时,会打印报警信息。

为保证ImpatientProxy对象有效,需要使用Proxy类来简化动态代理的创建过程,所以我们加入了newInstance方法:

	public static Object newInstance(Object obj){
		ClassLoader loader = obj.getClass().getClassLoader();
		Class[] classes = obj.getClass().getInterfaces();
		return Proxy.newProxyInstance(loader, classes, 
				new ImpatientProxy(obj));
		
	}

  能够在被截获方法调用执行前和执行后创建其他有意义的操作是面向方面编辑(AOP)的念之一。在AOP中,方法aspect就是建议和切入点的组合。

在JAVA中,动态代理技术使你能够使用代理对象包装其他对象,截获被包装对象的调用,在调用传递前后增加其他操作等,这样可以比较随意地给任何对象增加可复用的行为,从这点来讲,与AOP非常类似。

 

 

小结:

   代理模式的实现要求建立一个占位对象,用于控制对目标对象的访问, 这样客户端就无需了解目标对象的状态变化。就像加载一个图像需要一定时间时,我们可能使用代理模式改善用户体验。但代理模式本身存在代理对象与被代理对象之间的偶合程序过紧的问题。在JAVA中动态代理有进可以提供一种增加可复用功能的机制。如果某对象的类可实现要截获的接口,可以使用动态代理包装该对象,增加自己的处理逻辑,以增加或者替换被包装对象代码的功能。

 

 

参与资料:Java设计模式

               [美] Steven John Metsker

                      William C.Wake

1 楼 EyejavaLi 2010-03-04  
设计形式之略见一斑(代理模式Proxy)