Proxy代理模式

来源:互联网 发布:java面向对象设计原则 编辑:程序博客网 时间:2024/06/03 17:55

当别人给你了源包,当你是用该源包方法的时候,无法修改源代码(比如只给你一个编译过的class文件)来修改实现你所需要的功能,或者需要把业务代码和日志打印完全分离开,不想要业务代码里面混着日志代码,或者需要提供的java类什么地方是瓶颈,肯定需要在方法的前后加上一些代码,就需要用到代理模式

Tank

package Proxy;import java.util.Random;public class Tank implements Moveable{    @Override    public void move() {       System.out.println("Tanking moving....");       try {        Thread.sleep(new Random().nextInt(10000));        } catch (InterruptedException e) {            e.printStackTrace();        }       }    }

Tank的代理者们

//日志代理package Proxy;//和被代理对象实现一样的接口public class TankLogProxy implements Moveable{    Tank tank;    public TankLogProxy(Tank tank) {        super();        this.tank = tank;    }    @Override    public void move() {        //方法开始执行之前,可以进行检查、记录等操作        System.out.print("Tank..start");        tank.move();//--------------代理调用        //方法结束后可以写日志等。        System.out.print("Tank..stop");    }}
//时间代理package Proxy;import java.sql.Time;//和被代理对象实现一样的接口public class TankTimeProxy implements Moveable{    Tank tank;    public TankTimeProxy(Tank tank) {        super();        this.tank = tank;    }    @Override    public void move() {        //方法开始执行之前,可以进行检查、记录等操作        long start=System.currentTimeMillis();        tank.move();//--------------代理调用        //方法结束后可以写日志等。        long end=System.currentTimeMillis();        System.out.println(start-end);    }}

调用

public class Client {    public static void main(String[] args) {        Tank tank=new Tank();        TankLogProxy tlp=new TankLogProxy(tank);//把tank对象交给其代理对象        tlp.move();    }}

两个代理一起使用(时间代理包着日志代理)

package Proxy;public class Client {    public static void main(String[] args) {        Tank tank=new Tank();        TankLogProxy tlp=new TankLogProxy(tank);//把tank对象交给其代理对象        TankTimeProxy ttp=new TankTimeProxy(tlp);//以为都实现Moveable接口,把tlp日志代理对象交给时间代理对象        ttp.move(); //先调用时间日志的move,会执行获取时间,因为把tlp传给时间,所以move方法执行的是日志代理的move     }}

聚合的方式实现代理:
1、会和原来被代理对象实现一样的接口,代理者内部会聚合一个被代理对象。
2、所以,代理者里面可以装任何实现实现了某接口的对象,甚至是另外一个代理者。
这里写图片描述



问题:
1、当既需要时间代码,也需要日志代理,可以把它们组合在一起用,弄成一个新的代理者(各种不同的功能叠加,代理一个类就会包一层,再把该代理类代理,又会继续包一层,会使得继承或者聚合无限制的往下走,继承会产生类爆炸,聚合会产生好多层包下去),那如果还需要权限验证或者先执行日志后执行时间代理呢?
2、若Moveable接口里面增加一个方法,该方法也需要日志代理和时间代理,是不是还要在该方法内重写一遍打印时间和记录日志的操作呢?重写一个代理者?
3、若需要对任意一个对象进行时间代理和日志代理,而不仅仅只是Tank对象,比如,我想对Car实现,对Animal实现,难道还要全部写一遍各自的Proxy吗?比如如果需要对系统中任意对象进行运行时间的观察,那么又该怎样呢?

产生一个代理,可以对任何一个对象进行代理
以上代码中,代理者代理的对象都是实现了Moveable接口的对象。
所以,代理模式代理的对象都实现了某一个接口,所以真正代理的时候,是根据那个接口来实现代理的,而不是类
Spring中也是如此要求的,但是也可以不用接口实现代理,也可以用继承,但是Spring是不推荐这样做的。
AOP是动态代理的一种应用。

动态代理

模拟JDK实现动态代理
Moveable作为需要实现动态代理的接口

package Proxy;public interface Moveable {    void move();    void stop();}

编写Proxy类,用来作为产生代理对象的类(代理的总代理)

package Proxy;public class Proxy {    /**     *所有被代理对象都由该方法产生代理对象     * @return     */    public static Object newProxyInstance(){        String src="package Proxy;" +                "import java.sql.Time;" +                "public class TankTimeProxy implements Moveable{" +                "Moveable tank;" +                "   public TankTimeProxy(Moveable tlp) {" +                "   super();" +                "this.tank = tlp;" +                "}" +                "   @Override" +                "public void move() {" +                "//方法开始执行之前,可以进行检查、记录等操作" +                "   long start=System.currentTimeMillis();" +                "   tank.move();" +                "//方法结束后可以写日志等。" +                "long end=System.currentTimeMillis();" +                "   System.out.println(start-end);" +                "}" +                "   @Override" +                "public void stop() {" +                "   //方法开始执行之前,可以进行检查、记录等操作" +                " long start=System.currentTimeMillis();" +                "       tank.move();" +                "       //方法结束后可以写日志等。" +                "       long end=System.currentTimeMillis();" +                "       System.out.println(start-end);" +                "   }" +                "}";        return null;    }}

以上代码中,如果src代表的一段源代码可以被编译装入内存中,直接产生一个新的代理对象,那么项目中甚至可以不写TankTimeProxy 类,直接可以由Proxy类的newProxyInstance方法根据src动态的生成代理对象。

动态代理的思想:不需要再写什么TankTimeProxy,只需要调用Proxy的newProxyInstance方法就可以直接返回代理对象,也不需要什么代理对象的名字。
如何生成动态代理对象呢?要能够动态编译src的这段源代码,这样就可以在运行的时候想得到什么类就得到什么类。

动态代理就是为了解决业务层分级、解耦、类太多的问题。

动态编译:

1、JDK6 Complier API,用jdk本身就是用接口实现动态代理
2、网上现有的开源的东西,如CGLib、ASM(甚至不用源码编译,会直接生成二进制文件,因为java的二进制文件的格式也是公开的。如果能理解二进制文件的意思,甚至连编译器都不需要了。)
【Spring中就是用的CGLib、ASM】

注意:编译器API只有在JDK6中有,以前的没有

JDK6调用Complier API动态编译

package main;import java.io.File;import java.io.FileWriter;import java.io.IOException;import javax.tools.JavaCompiler;import javax.tools.ToolProvider;public class main {    public static void main(String[] args) throws IOException {        String src="package main;" +"\r\n"+                "import java.sql.Time;" +"\r\n"+                "import Proxy.Moveable;"+"\r\n"+                "public class TankTimeProxy implements Moveable{" +"\r\n"+                "Moveable tank;" +"\r\n"+                "   public TankTimeProxy(Moveable tlp) {" +"\r\n"+                "   super();" +"\r\n"+                "   this.tank = tlp;" +"\r\n"+                "}" +"\r\n"+                "   @Override" +"\r\n"+                "   public void move() {" +"\r\n"+                "   //方法开始执行之前,可以进行检查、记录等操作" +"\r\n"+                "   long start=System.currentTimeMillis();" +"\r\n"+                "   tank.move();" +"\r\n"+                "   //方法结束后可以写日志等。" +"\r\n"+                "   long end=System.currentTimeMillis();" +"\r\n"+                "   System.out.println(start-end);" +"\r\n"+                "}" +"\r\n"+                "   @Override" +"\r\n"+                "   public void stop() {" +"\r\n"+                "   //方法开始执行之前,可以进行检查、记录等操作" +"\r\n"+                "   long start=System.currentTimeMillis();" +"\r\n"+                "   tank.move();" +"\r\n"+                "   //方法结束后可以写日志等。" +"\r\n"+                "   long end=System.currentTimeMillis();" +"\r\n"+                "   System.out.println(start-end);" +"\r\n"+                "   }" +"\r\n"+                "}"+"\r\n";                //先把源代码写入一个临时文件中        String fileName=System.getProperty("user.dir");//获取当前文件的系统目录,就是项目的根路径        String temp=fileName+"/src/main/TankTimeProxy.java";//临时文件系统路径        File f=new File(temp);        FileWriter fw=new FileWriter(f);        fw.write(src);        fw.flush();        fw.close();        //动态编译        JavaCompiler complier=ToolProvider.getSystemJavaCompiler();//拿到编译器对象        StandardJavaFileManager fileMgr=complier.getStandardFileManager(null,null,null);//文件管理对象        Iterable units=fileMgr.getJavaFileObjects(temp);//得到需要编译的文件        CompilationTask t=complier.getTask(null,fileMgr,null,null,null,units);//一次编译任务        t.call();//调用编译任务        fileMgr.close();//关闭        //把class类文件load到内存中(用ClassLoader把class文件加载到内存,该class文件必须是在classPath下的,而自己写的动态 编译的class文件是在源码src下而不在bin里,        //所以load class不能用ClassLoader,要用URLClassLoader。如果把自己动态编译的class放到bin里,就会和编译器自动编译的混淆在一起)        URL[] urls=new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};//在找class的时候去该目录下找        URLClassLoader ul=new URLClassLoader(urls);        Class c=ul.loadClass("main.TankTimeProxy");//网上传来的class文件也可以这样load进来        //生成新对象,用到反射        Constructor ctr=c.getConstructor(Moveable.class);//拿到构造方法,表示找到一个参数是Moveable.class的构造方法        Moveable m=(Moveable) ctr.newInstance(new Tank());//Tank为被代理的对象        m.move();    }}


Moveable m=(Moveable) ctr.newInstance(new Tank());//Tank为被代理的对象
我们根本不知道具体的是什么类的代理对象,只知道是实现了Moveable接口的类,这就是动态生成代理类对象。



可能遇到的问题:
JavaCompiler:JDK6中新提供的一个类
JavaCompiler complier=ToolProvider.getSystemJavaCompiler();会拿到系统中默认的java编译器,其实就是javac。(如获取的时候报null,空指针异常,往往是由配置引起的,打开eclipse,在installed JRES中,jre是只包含纯运行环境,不包含编译的,要添加我们自己的jdk环境,而不是纯jre。
在非常多Java应用中需要在程序中调用Java编译器来编译和运行。但在早期的版本中(Java SE5及以前版本)中只能通过tools.jar中的com.sun.tools.javac包来调用Java编译器,但由于tools.jar不是标准的Java库,在使用时必须要设置这个jar的路径。而在Java SE6中为我们提供了标准的包来操作Java编译器,这就是javax.tools包。使用这个包,我们能不用将jar文件路径添加到classpath中了。

JDK6公开了Complier API,不用JDK6也可以进行动态编译,如使用编译命令(需要环境支持)或者使用Sun未公开的编译类。

0 0