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

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

体会设计模式

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

代理模式

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

提出问题

新建一个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();
       }
   }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值