您的位置:首页 > 其它

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

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

体会设计模式

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

代理模式

由一个实际问题来表达代理模式的思想

注:源代码在最后,过程中的代码不一定能运行,请参看源代码

提出问题

新建一个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");
}
}


另外一种方式通过聚合的代理

聚合,一个类中包含另外一个类。

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

接口代码:

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前后加上启动和停止怎么办?

以继承的方式实现,我们的思路是让它的代理类再被代理,代码如下:

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. 先计算时间再启动



2. 先启动再计算时间



以上就实现了一个静态代理。从上面的效果可以看出实现了预期的效果,我们可以任意的指定谁先代理谁后代理,可以看作是横向扩展了。

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

动态代理

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

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

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

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

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

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

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

首先我们需要一个代理对象:

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


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

我们首先要得到要有生成对象的代码,但是代码不能交给程序处理,所以我们要将代码转化成程序能处理的形式,那就是字符串。

用字符串表示代码后我们就可以任意的构造出我们想要的代码,让后将字符串输出到一个java文件中交给程序去编译

那程序要怎么编译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();
}
}


效果图:



从图中我们可以生成了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是由字符串生成的,而我们动态修改字符串是不是容易多了。接下来我们要做的就是修改字符串。

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

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

"public class TmpProxy implements "+inter.getName()+" {\n" +

"    public TmpProxy("+handler.getClass().getName()+" tmp){\n" +
"        this.tmp=tmp;\n" +
"    }\n" +


然后我们需要得到接口里的方法对代理类中的方法进行重新编排

//根据接口的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";


修改好后我们可以动态的生成任何对象的代理对象,只是生成的代理对象固定的只能统计运行时间业务,所以我们还需要一个处理业务的逻辑。

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

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

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();
}
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: