代理模式
代理模式应用在需要通过简单对象来描述复杂对象的时候,如果要创建一个需要花费很多时间或资源开销的对象时,代理可以让你延迟创建直到你真正需要这个对象为止。通常代理具有和它描述对象一样的方法,一旦对象被加载,他会传递这个通过代理到实际对象调用的方法。
下面是可以使用代理的几种情况:
1. 一个需要很长时间加载的对象,例如一个大的图片。
2. 对象在远程机器上通过网络加载会很慢,特别上网络下载的高峰期。
3. 授权机制:对象的访问权限被限制,代理可以验证用户的访问权限。
代理也可以被用来区分对象是请求还是真正需要访问它。例如程序初始化时会设定一些不会立刻用到的对象,这种情况下,代理可以在真正需要对象的时候加载它。
我们假设这样一种情形,程序要加载并显示一幅大的图片。当程序启动时,需要有迹象表明一幅图片将被加载,这样屏幕会作出相应的设置,但实际上图片的显示可以被延迟到图片加载完毕后。这一点在程序中非常重要,例如在字符处理器或者web浏览器中会在图像可以使用前显示文字。
一个图像代理可以记录并在后台加载它,同时会在显示图片前在屏幕上绘制一个简单的方格或者其他符号来表现图像的大小。代理甚至可以延迟加载图像直到它接收到绘制的请求,然后再开始这个过程。
两种机制
最初的教程中有关于图像加载的Prxoy代码,UI演示虽然比较直观,但对不了解GUI的程序员来说反而不清晰,下面我就个人的理解对代理模式的几个应用分别举例解释.
1. 授权机制
本例中我们假定用户要读取股市信息,但是这些信息需要一定的权限才能读取。通常的代码可以是在用户调用这个方法后对用户的权限进行验证,如果满足要求则读取数据并返回,否则返回null或者抛出异常。而在代理模式下,则是首先由代理来进行验证用户的权限来决定是否由被代理对象读取信息还是抛出异常。我们暂时先不讨论这两种方式的好坏。
Info接口,提供了一个readStockInfo方法,返回股票信息。
/* * @(#) IInfo * * Copyright 2001-2005 东软软件股份有限公司 * http://www.neusoft.com */ package designpattern.proxy.self;
/** * IInfo * <p> * 信息接口,提供可以读取的信息 * * @author li-xj li-xj@neusoft.com * @version 1.0 * @Date 2006-5-12 */ public interface IInfo { public String readStockInfo();
} |
IInfo的Info实现
package designpattern.proxy.self; public class Info implements IInfo{ /** * 返回股市信息 */ public String readStockInfo() { return "157股明日暴涨"; }
} |
IInfo的代理模型实现
package designpattern.proxy.self; public class InfoRroxy implements IInfo { IInfo info; public InfoRroxy(IInfo info) { this.info =info; } public String readStockInfo() { if(User.getUserPopedom() == User.TIPTOP_USER) return info.readStockInfo(); else return "你没有查看股市行情的权限"; }
} |
User是一个模拟用户类,模拟当前用户并返回当前用户的权限级别。代理通过权限来决定是否执行被代理对象的方法。这个类有两个静态方法:setCurrentUser可以设置当前用户,getUserPopedom返回当前用户的权限。
package designpattern.proxy.self; public final class User { public static final int TIPTOP_USER=1; public static final int OTHER_USER =2; private static String currentUser; public static int getUserPopedom(){ if(null != currentUser && currentUser.equals("li-xj")) return 1; else return 2; } public static void setCurrentUser(String userName){ currentUser = userName; } }
|
创建一个测试类,通常情况下不会由客户直接创建Info对象,而是通过工厂方法来得到客户所直接交互的InfoRroxy对象,客户会与InfoRroxy对象直接通讯,而InfoRroxy对象作为与Info对象的一个媒介。为了方便起见省略了我们省略了这个过程,在ProxyTest中显式创建了这两个对象。
当模拟用户为Linux时代理会拒绝用户的访问,而为li-xj时则能够得到当前股票的信息。
package designpattern.proxy.self; public class ProxyTest {
/** * @param args */ public static void main(String[] args) { String stockInfo; IInfo info = new Info(); IInfo infoproxy = new InfoRroxy(info);
User.setCurrentUser("Linux"); stockInfo =infoproxy.readStockInfo(); System.out.println("Linux,"+stockInfo);
User.setCurrentUser("li-xj"); stockInfo =infoproxy.readStockInfo(); System.out.println("li-xj,"+stockInfo); }
} |
2. ProxyDisplay是代理模式么?
设计模式中关于代理模式提供了ProxyDisplay这样一个基于Swing的图片显示程序,下面是它的源码:
package designpattern.proxy; //imports public class ProxyDisplay extends JxFrame { public ProxyDisplay() { super("Display proxied image"); JPanel p = new JPanel(); getContentPane().add(p); p.setLayout(new BorderLayout()); ImageProxy image = new ImageProxy(this.getClass(). getResource("/designpattern/proxy/elliott.jpg").getFile(), 321, 271); p.add("Center", image); p.add("North", new Label(" ")); p.add("West", new Label(" ")); setSize(370, 350); setVisible(true); } //------------------------------------ static public void main(String[] argv) { new ProxyDisplay(); } } //================================== class ImageProxy extends JPanel implements Runnable { int height, width; MediaTracker tracker; Image img; JFrame frame; Thread imageCheck; //to monitor loading //------------------------------------ public ImageProxy(String filename, int w, int h) { height = h; width = w;
tracker = new MediaTracker(this); img = Toolkit.getDefaultToolkit().getImage(filename); tracker.addImage(img, 0); //watch for image loading
//imageCheck = new Thread(this); //imageCheck.start(); //start 2nd thread monitor
//this begins actual image loading try{ tracker.waitForID(0,10); } catch(InterruptedException e){} imageCheck = new Thread(this); imageCheck.start(); //start 2nd thread monitor
} //------------------------------------ public void paint(Graphics g) { super.paint(g); if (tracker.checkID(0)) { height = img.getHeight(frame); //get height width = img.getWidth(frame); //and width
g.setColor(Color.lightGray); //erase box g.fillRect(0,0, width+5, height+5); g.drawImage(img, 0, 0, this); //draw loaded image } else { //draw box outlining image if not loaded yet g.setColor(Color.red); g.drawRect(1, 1, width+10, height+10); } } //------------------------------------ public Dimension getPreferredSize() { return new Dimension(width, height); } //public int getWidth() {return width;} //public int getHeight(){return height;} //------------------------------------ public void run() { //this thread monitors image loading //and repaints when done //the 1000 msec is artifically long //to allow demo to display with delay try{ //Thread.sleep(1000); while(! tracker.checkID(0)){ Thread.sleep(1000); } } catch(Exception e){} repaint(); } } |
程序初始化的时候会创建一个image对象,MediaTracker 类是一个跟踪多种媒体对象状态的实用工具类。媒体对象可以包括音频剪辑和图像,但目前仅支持图像。tracker.waitForID(0,10)开始加载由此媒体跟踪器跟踪且具有指定标识符(0)的所有图像。在完成加载具有指定标识符的全部图像之前,或在 ms 参数以毫秒指定的时间(10ms)到期之前,此方法一直等待。
闲话短说,ImageProxy实现Runnable接口,这样做的目的是另起一个线程来根据图像是否加载完毕来延迟Panel的刷新。Thread.sleep(1000);将线程暂停1000ms,但是这个线程跟构造方法ImageProxy(String filename, int w, int h)不是同一个线程,所以并不影响tracker.waitForID(0,10)的执行,也就是加载图像。它的效果是在1000ms后验证图像是否加载完,如果已经加载完毕,那么就调用repaint();方法,否则就继续等待下一验证。
可能很多人会疑惑,为什么利用paint()已经对图像是否加载完做了验证,何必再多次一举呢?其实第一次调用paint()方法的时候,图像并没有加载完毕(如果要加载完毕,代理模式也就没有意义了),额外的我们讨论一下两个问题:
tracker.waitForID方法的等待时间的优先级是ms 参数而不是真正意义上的加载时间,这样其实是很有好处的,我们的程序不会在无限制的等待加载,而是可以继续执行其它的操作(当然,也可以利用线程来完成)。
Panel的重绘,Jpanel的重绘方法是发成在两种情况下,一是窗口由不可视变为可是,二是窗口的大小发生变化。其他的情况下并不会调用repaint()方法。
言规正传,ProxyDisplay成为代理模式的真正含义是它利用run()方法完成了对Panel的repaint()方法的代理,它根据图像加载的进度来决定是否调用panel的重绘方法。因此我们可以认定ProxyDisplay是代理模式,只不过它不是很规范化的代理模式。在图形界面里的实现真正的代理模式是很有难度的,因此我们并不追求无论何时何地都去盲目的套用模式。
动态代理
JAVA提供了一个Proxy类和一个InvocationHandler,这两个类都在java.lang.reflect包中。程序员通过实现java.lang.reflect.InvocationHandler接口提供一个执行处理器,然后通过java.lang.reflect.Proxy得到一个代理对象,通过这个代理对象来执行商业方法,在商业方法被调用的同时,执行处理器会被自动调用。
实现第一个例子
我们上述读股票的例子就可以不用写代理类而是实现InvocationHandler接口来提供一个处理器,这个处理器可以复用给其它的对象:
package designpattern.proxy.self; //imports; public class DynamicProxyHandler implements InvocationHandler{ private Object original; public DynamicProxyHandler(Object original){ this.original = original; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("INFO ->Before invoke the method of Info "); Object object = method.invoke(original, args); System.out.println("INFO ->After invoke the method of Info "); return object; } } |
下面是测试动态代理得代码:
package designpattern.proxy.self; public class DynomicProxyTest { public static void main(String args[]){ IInfo info = new Info(); //在这里指定被代理类 InvocationHandler handler = new DynamicProxyHandler(info); //初始化代理类 IInfo info2 = (IInfo) Proxy.newProxyInstance(DynomicProxyTest.class .getClassLoader(),new Class[]{IInfo.class}, handler); System.out.println(info2.readStockInfo()); } } |
程序首先创建一个被代理对象然后初始化代理对象,最后由Proxy的newProxyInstance获得一个代理实例, newProxyInstance方法有三个参数, ClassLoader, Class[] interfaces, InvocationHandler。ClassLoader是当前的类加载器,interfaces是代理实例要实现的接口的类(需要指出java的Proxy是针对接口的,如果传入的Class类型不是接口,会抛出IllegalArgumentException异常)。InvocationHandler则是我们写的代理处理器。newProxyInstance获得的实例对象的类是通过修改java二进制文件来获得的,Proxy中有一个private static native Class defineClass0(ClassLoader loader, String name,byte[] b, int off, int len);方法,这个本地方法实现了这个Class的生成,至于具体的实现我们无法看到,不过很多资料上都有关于native方法的讨论。
虽然我们看不到Proxy所生成的类的源文件,但我们可以根据newProxyInstance方法的部分代码和返回的结果来推断一下,它是一个含有handler属性并且实现了我们所传入的接口的一个类:
package designpattern.proxy.self;
import java.lang.reflect.InvocationHandler;
public class $Proxy0 implements IInfo { private InvocationHandler handler;
public $Proxy0(InvocationHandler handler) { this.handler = handler;
}
public String readStockInfo() { try { return (String)handler.invoke(this,this.getClass().getMethod("readStockInfo", new Class[1]{}),null); } catch (Throwable e) { e.printStackTrace(); return null; } }} |
这个是我根据上下文信息猜测出的代理的源代码,实际上这是不存在的,存在的只是一个二进制文件(.class),不幸的是它只是存在于内存里。就在写下这些的时候,我发现java.lang.Class是继承了Serializable接口的,那么我们就可以将它序列化到本地文件中,于是我用
FileOutputStream fos = new FileOutputStream(proxy.getClass().getName()+".class1");
ObjectOutputStream oos = new ObjectOutputStream(fos); oos.writeObject(proxy.getClass());
oos.flush();
oos.close();
将这个类的二进制文件保存了下来,用反编译工具得到了一个很让人开心的结果,这是native方法生成的类的源码:
// Decompiled by Jad v 1.5.8 f. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://www.kpdus.com/jad.html // Decompiler options: packimports(3) fieldsfirst ansi // Source File Name: $Proxy0.java
import designpattern.proxy.self.IInfo; import java.lang.reflect.InvocationHandler;
public class $Proxy0 implements IInfo {
private InvocationHandler handler;
public $Proxy0(InvocationHandler handler) { this.handler = handler; }
public String readStockInfo() { try { return (String)handler.invoke(this, getClass().getMethod("readStockInfo", new Class[1]), null); } catch(Throwable e) { e.printStackTrace(); } return null; } } |
几乎完全一样,其实有时候事实就是这样简单。所以在很多看不到的事情时,要相信自己的思考。笔者也是一个很喜欢钻牛角尖的人,正如一定要知道Proxy的工作机制。
性能的思考
动态代理能解决所有代理模式的问题么?因为传统代理有两个坏处:要几乎重写一个新的类,增加近一倍的对象数目(当然动态代理恐怕占有的更多)。不过它也有好处,代码很清晰,每个方法都可以单独写自己授权限制,比如在Info中如果还有一个读取天气的方法readWeather,他可以给任何权限的人去调用。使用传统的代理我们可以很容易的在代理方法中不去限制,但如果使用动态代理,我们就不得不去添加很复杂的逻辑去判断是否限制。而且动态代理的效率是很有折扣的,我们可以很直观的从它方法调用逻辑的复杂度上推测到,而这也可能成为你程序性能的瓶颈。
不过动态代理的确可以减少代码量的维护,假如你只是对用户的访问做一个日志,那么你只需要在invoke方法体中写下两行代码就能够对所有被代理对象的方法调用做下日志,这在传统代理中去实现是一件工作量多么重复性浪费的活啊!我对AOP(面向方面编程)并不了解,但偶尔看到说逻辑与日志、事务等分开,而动态代理就能很好的做到这一点,起码在日志上。