详述代理模式及动态代理简单实现

来源:互联网 发布:男士休闲短靴 知乎 编辑:程序博客网 时间:2024/05/21 09:40

前言:本文章总结于马士兵老师系列教程,是根据视频中提出的问题的思维为大纲写的

体会设计模式

可能接触过设计模式的人都会有一种疑惑:感觉很多设计模式的实现方式都非常的相似,就比如说代理模式和装饰模式。确实有些设计模式的实现方式是差不多的,但是他们是从不同的场景出发,解决不同的问题的,我们需要从思想的角度来体会设计模式。

代理模式

由一个实际问题来表达代理模式的思想
注:源代码在最后,过程中的代码不一定能运行,请参看源代码

提出问题

新建一个Car类,包含一个drive方法,现在要求是在不改变Car代码的前提下计算drive运行的时间
Car代码:

public class Car {    public void drive() throws InterruptedException {        System.out.println("开车了...");        Thread.sleep(1000);    }}

解决统计时间的问题

如果要统计时间,我们现在要将代码写成这样:

public class Car {    /**     * 解决这个问题我们需要在drive运行开始和结束的位置加入系统当前时间     * 再得到两个时间差     * @throws InterruptedException     */    @Test    public void drive() throws InterruptedException {        //获取开始时间点        long i = System.currentTimeMillis();        System.out.println("开车了...");        Thread.sleep(1000);        //获取结束时间点        long j = System.currentTimeMillis();        System.out.println("车开了"+(j-i)+"millis");    }}

解决不能使用改动drive代码的问题

现在是不能改动drive的那要怎么把计算时间的代码插入进去呢?
这个时候我们就需要用到代理模式了,Car的代码不能改但是它的代理类可以,而代理方式也有两种如下:
1. 通过继承Car来代理

public class ExtendsCar extends Car {    //这样我们可以通过重写Car的drive方法来实现代理    @Override    public void drive() throws InterruptedException {        long i = System.currentTimeMillis();        super.drive();        long j = System.currentTimeMillis();        System.out.println("车开了:"+(i+j)+"millis");    }}
  1. 另外一种方式通过聚合的代理

聚合,一个类中包含另外一个类。
用聚合的方式代理时,我们需要代理类和被代理类有同样的行为,我们可以通过实现相同的接口来实现

接口代码:

public interface Move {    public void drive();}

让Car类implement接口,这里就不贴代码了
代理类实现接口:

public class TimeCarProxy implements Move {    Car c ;    public TimeCarProxy(Car c){        this.c=c;    }    @Override    public void drive() {        long i = System.currentTimeMillis();        System.out.println("开始时间:" + i);        move.drive();        long j = System.currentTimeMillis();        System.out.println("结束时间:" + j);        System.out.println("车开了:"+(j-i)+"millis");    }}

问题:两种实现方式那个比较好?

这里还是以一个问题来表达:

如果我还要在前面drive前后加上启动和停止怎么办?

  1. 以继承的方式实现,我们的思路是让它的代理类再被代理,代码如下:
public class CarAction extends ExtendsCar {    @Override    public void drive() {        System.out.println("启动车!");        super.drive();        System.out.println("停止车!");    }}
这种方式的问题:    1. 继承被占用,不能再继承其它类    2. 主要的问题:如果我要将他们的顺序反过来,先启动停止在计算时间的话,那不就意味着我们要重新写它的代理类吗?此时继承的局限性就显现出来了

2. 以聚合的方式实现:我们可以再来一个代理实现Move接口,代码如下:

public class Action implements Move {    ImplementMove move ;    public Action(ImplementMove move){        this.move=move;    }    @Override    public void drive() {        System.out.println("车启动!");        move.drive();        System.out.println("车停止!");    }}

到这里可能会有疑问,这不是跟继承存在一样的问题吗?别急,请看下面一种实现:

public class ActionCarProxy implements Move {    /**     * 因为它们有一个共同的特点就是实现了Move,若我们把聚合对象换成     * Move接口不就想让谁代理就谁代理了吗?     * 同样我们也要将代理时间的类改成Move     */    Move move ;    public ActionCarProxy (Move move){        this.move=move;    }    @Override    public void drive() {        System.out.println("车启动!");        move.drive();        System.out.println("车停止!");    }}

可能会有点绕,这里写个测试类来理一理思路,测试类代码:

public class TestCar {   public static void main(String args[]){       Car car = new Car();       //若我们想先开始计算时间再启动停止//       TimeCarProxy t = new TimeCarProxy(car);//       ActionCarProxy a = new ActionCarProxy(t);//       a.drive();       //若我们想先启动停止再计算时间       ActionCarProxy a = new ActionCarProxy(car);       TimeCarProxy t = new TimeCarProxy(a);       t.drive();   }}

运行效果:
1. 先计算时间再启动
proxy1
2. 先启动再计算时间
proxy2
以上就实现了一个静态代理。从上面的效果可以看出实现了预期的效果,我们可以任意的指定谁先代理谁后代理,可以看作是横向扩展了。

到这里会有很多人有疑惑,这不是装饰设计模式吗?从实现语法的角度来讲确实是很像装饰,但是两者的着重点不同,装饰模式旨在扩展功能,这里是以代理Car类去解决问题,语义,出发点是不同的,前面讲到过设计模式的体会,这里还需要读者慢慢去体会。

动态代理

注意:这里开始模仿JDK实现Proxy

这里跟着上面的思路来,我们引入一个新的问题:

如过Car里有多个方法要求计算运行时间怎么处理?

这样的话我们需要在TimeCarProxy 中的每个方法前后获取当前时间并计算,那这样的话我们会发现TimeCarProxy中出现了很多的重复代码,当然我们可以给重复的代码简单封装,当那也没从根本上解决问题。

这个问题暂且搁置,先看下一个问题,这个时候请注意,请将重点放到TimeCarProxy 上来。

如果我们需要TimeCarProxy 不仅代理Car还能代理其它类对象

也就是一个万能的TimeProxy代理,可以代理任意对象执行计算方法运行时间,这个时候我们需要怎么办?
首先我们需要一个代理对象:

//jdk中Proxy就是动态代理public class Proxy {    //产生并返回一个代理对象    public static Object newProxyInstance(){        //我们需要在这里动态的生成代理对象        return null;    }}

那我们要怎么动态生成对象呢?


  1. 我们首先要得到要有生成对象的代码,但是代码不能交给程序处理,所以我们要将代码转化成程序能处理的形式,那就是字符串。
  2. 用字符串表示代码后我们就可以任意的构造出我们想要的代码,让后将字符串输出到一个java文件中交给程序去编译
  3. 那程序要怎么编译java文件呢?

JDK6为我们提供了Complier API ,另外还有CGlib、ASM插件可以直接生成二进制文件不用再编译了,Spring中也支持通过CGlib方式实现动态代理
现在暂时不管生成字符串的逻辑,我们先解决编译的问题
代码:

public class FileTest {    @Test    public void test() throws IOException {        //用来生成代理对象的代理类的字符串形式        String src="" +                "package net.hncu.test;\n" +                "public class TmpProxy implements Move {\n" +                "    Move move ;\n" +                "    public TmpProxy(Move move){\n" +                "        this.move=move;\n" +                "    }\n" +                "    @Override\n" +                "    public void drive() {\n" +                "        long i = System.currentTimeMillis();\n" +                "        System.out.println(\"开始时间:\" + i);\n" +                "        move.drive();\n" +                "        long j = System.currentTimeMillis();\n" +                "        System.out.println(\"结束时间:\" + j);\n" +                "        System.out.println(\"车开了:\"+(j-i)+\"millis\");\n" +                "    }\n" +                "}";        //将字符串保存成一个java文件System.getProperty("user.dir")获得项目路径        String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java";        //新建文件        File file = new File(filename);        //将字符串写到文件        FileWriter fileWriter =new FileWriter(file);        fileWriter.write(src);        fileWriter.close();         /**         *  编译java文件这里对编译过程不做过多阐述,如果感兴趣可以去查看api         */        //获取java编译器jdk6支持        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();        //获取一个file管理器(三个参数diagnosticListener监听编译过程的监听器        // locale国际化相关,charset指定字符集)所有参数为空时,指定默认配置        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);        //根据文件名字拿到java文件对象(可以填多个文件,获得多个对象)返回一个文件对象的迭代器        Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename);        //建立一次编译任务(参数:out输出位置,fileManager文件管理器,diagnosticListener监听器,        // options编译时的参数,暂不填,classes编译时所需要的class文件,compilationUnits需要编译的单元)        JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units);        //执行编译        task.call();        //关闭文件管理器        fileManager.close();    }}

效果图:
proxy3
从图中我们可以生成了TmpProxy 的java文件和class文件
接下来我们又要考虑下一个问题了。

我们需要把我们生成的class文件加载到内存来生成一个代理对象

这里只贴部分代码:

 //加载class 文件到内存,        //直接从指定URL位置加载class文件到内存,其实我们也可以直接将class存到bin目录下,但是可能会造成冲突        // 首先我们需要一个URL数组指定加载class文件的路径,        URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};        //新建一个URL类加载器        URLClassLoader classLoader = new URLClassLoader(urls);        //加载路径下的指定class文件        Class aClass = null;        try {            aClass = classLoader.loadClass("net.hncu.test.TmpProxy");        } catch (ClassNotFoundException e) {            e.printStackTrace();        }        //System.out.println("aClass: "+aClass+" from: "+"FileTest.test");        //利用反射操作class对象        //构造一个实例        Constructor constructor = aClass.getConstructor(inter);        return  constructor.newInstance(object);

好了,到此为止,我们达到了动态生成代理对象的目的了,但是我们会发现还是只能动态生成TimeProxy,但是你别忘了,TimeProxy是由字符串生成的,而我们动态修改字符串是不是容易多了。接下来我们要做的就是修改字符串。

问题来了,我们需要如何修改字符串让其动态生成我们需要的代码呢?

  1. 首先我们需要生成任意对象的代理类,我们需要告诉它我们要生成代理类的规范,即被代理类的接口

    "public class TmpProxy implements "+inter.getName()+" {\n" +                "    public TmpProxy("+handler.getClass().getName()+" tmp){\n" +                "        this.tmp=tmp;\n" +                "    }\n" +
  2. 然后我们需要得到接口里的方法对代理类中的方法进行重新编排

    //根据接口的class文件动态实现多个方法的代理字符串拼接
    String methodStr="";
    Method[] methods = inter.getMethods();
    for (Method method : methods) {
    methodStr+=
    " @Override\n" +
    //这里方法名要改成当前的方法名
    " public void "+method.getName()+"() {\n" +
    " try{\n"+
    " Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+
    " tmp.invoke(this,method);\n" +
    " }catch(Exception e){\n"+
    " e.printStackTrace();\n"+
    " }\n"+
    " }\n";
  3. 修改好后我们可以动态的生成任何对象的代理对象,只是生成的代理对象固定的只能统计运行时间业务,所以我们还需要一个处理业务的逻辑。

那么我们需要怎样来修改业务逻辑呢?

因为业务逻辑是需要用户自己来定义的,所以不能写死在字符串中,当是业务逻辑需要有一定的编写规范,所以最好的选择就是通过一个接口来规范业务逻辑处理,让后让用户来实现接口定义自己的业务逻辑。

public interface ProxyHandler {    //在自定义方法模块时我们肯定要执行被代理类本身的方法,    //所以我们至少需要以下两个参数    public void invoke(Object object,Method m);}

分析:该接口定义了目标类方法的实现规则,所以我们在实现该接口的时候需要告知它目标类。
实现代码例子:

public class TimeHandler implements ProxyHandler {    Object target;    public TimeHandler(Object target){        this.target=target;    }    @Override    public void invoke(Object object,Method m) {        long start = System.currentTimeMillis();        System.out.println("开始时间:"+start);        try {            m.invoke(target);        } catch (IllegalAccessException e) {            e.printStackTrace();        } catch (InvocationTargetException e) {            e.printStackTrace();        }        long end= System.currentTimeMillis();        System.out.println("结束时间:"+end);        System.out.println("用时:"+(end-start));    }}

注意点:Proxy中的invoke实际上是调用的是handler中的invoke
如此一来,就可以实现处理任何的业务逻辑了,同时简单的代理模式也实现了。

结论

目前这个只是简单的实现,只能做没有参数列表、返回值的,后续有时间再去完善

源码

注意:

在第一遍运行时会出现ClassNoFound异常,那是因为编译的class文件并没有马上写到目录下,重新运行就可以出来结果了,至于如何改这个bug我还没研究出来。
Car:

package net.hncu.test;import org.junit.Test;/** * Project: String1 * Desc: proxy test * Author: AMX50B * Date: 2017-10-20 19:08 */public class Car implements Move {    /**     * 解决这个问题我们需要在drive运行开始和结束的位置加入系统当前时间     * 再得到两个时间差     */    @Test    public void drive(){        System.out.println("开车了...");        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }    @Override    public void flay() {        System.out.println("起飞了...");        try {            Thread.sleep(1000);        } catch (InterruptedException e) {            e.printStackTrace();        }    }}

Move:

package net.hncu.test;/** * Created by AMX50B on 2017/10/20 */public interface Move {    public void  drive();    public void  flay();}

Proxy:

package net.hncu.test;import org.junit.Test;import javax.tools.JavaCompiler;import javax.tools.JavaFileObject;import javax.tools.StandardJavaFileManager;import javax.tools.ToolProvider;import java.io.File;import java.io.FileWriter;import java.io.IOException;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;import java.net.URL;import java.net.URLClassLoader;/** * Project: String1 * Desc: proxy * Author: AMX50B * Date: 2017-10-21 12:51 */public class Proxy {    //产生并返回一个代理对象    @Test    public  static Object newProxyInstance(Class inter,ProxyHandler handler) throws IOException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {        //用来生成代理对象的代理类的字符串形式        //根据接口的class文件动态实现多个方法的代理字符串拼接        String methodStr="";        Method[] methods = inter.getMethods();        for (Method method : methods) {            methodStr+=                    "    @Override\n" +                    //这里方法名要改成当前的方法名                    "    public void "+method.getName()+"() {\n" +                    "       try{\n"+                    "         Method method = "+inter.getName()+".class.getMethod(\""+method.getName()+"\");\n"+                    "         tmp.invoke(this,method);\n" +                    "         }catch(Exception e){\n"+                    "              e.printStackTrace();\n"+                    "         }\n"+                    "    }\n";        }        String src="" +                "package net.hncu.test;\n" +                "import java.lang.reflect.Method;\n"+                //传入接口名称,使代理类能动态代理任意我们指定的接口                "public class TmpProxy implements "+inter.getName()+" {\n" +                "    "+handler.getClass().getName()+" tmp ;\n" +                "    public TmpProxy("+handler.getClass().getName()+" tmp){\n" +                "        this.tmp=tmp;\n" +                "    }\n" +               methodStr +                "}\n";        //将字符串保存成一个java文件System.getProperty("user.dir")获得项目路径        String filename=System.getProperty("user.dir")+"/src/net/hncu/test/TmpProxy.java";        //新建文件        File file = new File(filename);        //将字符串写到文件        FileWriter fileWriter =new FileWriter(file);        fileWriter.write(src);        fileWriter.close();        /**         *  编译java文件这里对编译过程不做过多阐述,如果感兴趣可以去查看api         */        //获取java编译器jdk6支持        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();        //获取一个file管理器(三个参数diagnosticListener监听编译过程的监听器        // locale国际化相关,charset指定字符集)所有参数为空时,指定默认配置        StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,null,null);        //根据文件名字拿到java文件对象(可以填多个文件,获得多个对象)返回一个文件对象的迭代器        Iterable<? extends JavaFileObject> units = fileManager.getJavaFileObjects(filename);        //建立一次编译任务(参数:out输出位置,fileManager文件管理器,diagnosticListener监听器,        // options编译时的参数,暂不填,classes编译时所需要的class文件,compilationUnits需要编译的单元)        JavaCompiler.CompilationTask task = compiler.getTask(null,fileManager,null,null,null,units);        //执行编译        task.call();        //关闭文件管理器        fileManager.close();        //加载class 文件到内存,        //直接从指定URL位置加载class文件到内存,其实我们也可以直接将class存到bin目录下,但是可能会造成冲突        // 首先我们需要一个URL数组指定加载class文件的路径,        URL[] urls = new URL[]{new URL("file:/"+System.getProperty("user.dir")+"/src")};        //新建一个URL类加载器        URLClassLoader classLoader = new URLClassLoader(urls);        //加载路径下的指定class文件        Class aClass = null;        try {            aClass = classLoader.loadClass("net.hncu.test.TmpProxy");        } catch (ClassNotFoundException e) {            try {                Thread.sleep(1000);                aClass= classLoader.loadClass("net.hncu.test.TmpProxy");            } catch (InterruptedException e1) {                e1.printStackTrace();            } catch (ClassNotFoundException e1) {                e1.printStackTrace();            }        }        //System.out.println("aClass: "+aClass+" from: "+"FileTest.test");        //利用反射操作class对象        //构造一个实例        Constructor constructor = aClass.getConstructor(handler.getClass());        return  constructor.newInstance(handler);//        move.drive();    }}

ProxyHandler:

package net.hncu.test;import java.lang.reflect.Method;/** * Created by AMX50B on 2017/10/23 */public interface ProxyHandler {    //在自定义方法模块时我们肯定要执行被代理类本身的方法,    //所以我们至少需要以下两个参数    public void invoke(Object object,Method m);}

TimeProxy:

package net.hncu.test;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Method;/** * Project: String1 * Desc: time handler * Author: AMX50B * Date: 2017-10-23 18:40 */public class TimeHandler implements ProxyHandler {    Object target;    public TimeHandler(Object target){        this.target=target;    }    @Override    public void invoke(Object object,Method m) {        long start = System.currentTimeMillis();        System.out.println("开始时间:"+start);        try {            m.invoke(target);        } catch (IllegalAccessException e) {            e.printStackTrace();        } catch (InvocationTargetException e) {            e.printStackTrace();        }        long end= System.currentTimeMillis();        System.out.println("结束时间:"+end);        System.out.println("用时:"+(end-start));    }}

Test:

package net.hncu.test;/** * Project: String1 * Desc: test car * Author: AMX50B * Date: 2017-10-21 9:58 */public class TestCar {   public static void main(String args[]){       Car car = new Car();       //若我们想先开始计算时间再启动停止//       TimeCarProxy t = new TimeCarProxy(car);//       ActionCarProxy a = new ActionCarProxy(t);//       a.drive();       //若我们想先启动停止再计算时间//       ActionCarProxy a = new ActionCarProxy(car);//       TimeCarProxy t = new TimeCarProxy(a);//       t.drive();       try {           ProxyHandler proxyHandler = new TimeHandler(car);           Move m = (Move) Proxy.newProxyInstance(Move.class,proxyHandler);           m.drive();           m.flay();       } catch (Exception e) {           e.printStackTrace();       }   }}
原创粉丝点击