设计模式--超详细 手动实现 jdk动态代理 原理(3)

在上一篇中,我们通过IO将文件写出的方式,实现了用户可以无感知的获取到代理对象,但是在上一篇的最后我们也提出了这种实现方式不灵活的地方,这两个点是必须要改进的,否则也就无法称之为“动态”代理。

改进1:动态生成用户所需类型的代理对象

我们希望能返回任何类型的代理对象,可以基于多态来改进此方法,因此将方法的参数修改成如下形式:


public class MyProxy {
       
    public static Object newProxyInstance(Class clazz);
}

对之前的实现做以下改动,相应的解释在代码注释中进行说明:

public class MyProxy {

    public static Object newProxyInstance(Class clazz) throws Exception {

        // 换行符
        String rt = "\r\n";

        /* 
          由于具体的类型是由用户通过参数传给我们,我们也就无法知道用户传给我们的到底是什么类,
          也不知道类中到底有什么方法,因此我们只能通过遍历这个类中的方法来获取方法,获取到方法
          的名称后,通过字符串的拼接来形成对该方法的调用,并且在方法调用前后加上日志的记录。
        */
     
        // 定义要拼接的方法字符串
        String methodStr = "";
	    
        // 获取clazz中的所有方法
        Method[] methods = clazz.getMethods();
	for (Method method : methods) {
		methodStr += "	@Override" + rt +
			     "	public void " + method.getName() + "() {"  + rt +
			     "    System.out.println(\"logger is start!\");" + rt +
			     " 	  target." + method.getName() + "();" + rt +
			     "	  System.out.println(\"logger is end!\");" +  rt +
			     "	}" + rt;
		}


	String src =
            "package cn.rain.design.proxy.demo2;" + rt +
             // 这里不再是导入Movable了,需要修改成用户传进来的clazz的包路径
             "import " + clazz.getName() + ";" + rt + rt +
             // 这里同样不是再实现Movable接口,而是用户传入的clazz,下面同理。
             "public class TempProxy implements " + clazz.getSimpleName() + "{" + rt + rt +
             "   private " + clazz.getSimpleName() + " target;" + rt + rt +
             // 构造此代理对象时,需要传入被代理对象,从而对被代理对象进行方法调用和增强"
             "   public TempProxy(" + clazz.getSimpleName() + " target) {" + rt +
             "       this.target = target;" + rt  +
             "	 }" + rt + rt +
             // 在这里将此前写死的move方法替换成上边通过遍历clazz方法而获得的字符串
             methodStr + 
             "}" ;


        /* 下边除了倒数第二行,其他都无变化 */
	
	String projectPath = System.getProperty("user.dir");
		
	String filePath = projectPath + "/src/main/java/cn/rain/design/proxy/demo2/TempProxy.java";
 
	File file = new File(filePath);
	FileWriter writer = new FileWriter(file);
	writer.write(src);
	writer.flush();
	writer.close();
 
	JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();

	StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
 
	Iterable<? extends JavaFileObject> targetFiles = fileManager.getJavaFileObjects(filePath);
		
	JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, targetFiles);
	task.call();
	fileManager.close();
 
	URL[] urlArr = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
	URLClassLoader urlClassLoader = new URLClassLoader(urlArr);
	Class<?> loadClazz = null;
	try {
		loadClazz = urlClassLoader.loadClass("cn.rain.design.proxy.demo2.TempProxy");
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	}finally {
		urlClassLoader.close();
	}

	// 通过反射获取实例,这里要将之前写死的Movable.class改成用户传入的clazz
	Constructor<?> constructor = loadClazz.getConstructor(clazz);
        // 这是是我们在创建该代理对象的时候写死的被代理对象new Car(),这是不合理的地方,
        // 但是我们先不去管它,后边会进行优化
	return constructor.newInstance(new Car());
	}
}

同之前一样,通过Main进行测试,不同的是这次要传入想要生成的代理对象的类型:

public class CompileTest {
	public static void main(String[] args) throws Exception {
        // 传入想要生成代理对象的类型
        Movable moveProxy = (Movable) MyProxy.newProxyInstance(Movable.class);
        moveProxy.move();
	}
}

运行后得到我们通过字符串写出的TemoProxy.java和编译后的TemoProxy.class:

我们发现最终生成的代码和之前的版本没什么区别,但是我们在使用上比之前更加灵活:

  • 之前由于类型是写死的,故只能代理实现了Movable接口类型的对象;现在可以代理任何类型的对象。
  • 之前由于方法是写死的,故只能对Movable.move()方法进行增强;现在可以动态获取传入的clazz中的所有方法,并对它们进行增强。

至此,我们已经实现了改进1,可以动态生成用户所需类型的代理对象,下面通过时序图来看一下我们都做了什么:

虽然我们此时生成代理对象的方式已经比之前灵活了许多,但还是存在一些不合理的地方:

  • 目前对方法的增强依然是写死的,只能实现记录日志的方法增强,如果用户需要记录时间,难道还要再写一个这样的Proxy吗?
  • 在MyProxy最后一行,我们通过反射创建实例的时候(即时序图中的第8步),在调用TempProxy的构造方法时是写死new Car()进行实例创建的,这很不合理。用户想要一个Movable类型的代理对象,我们却直接给了用户一个代理了Car的代理对象TempProxy,虽然Car确实是Movable类型的,但是你怎么知道用户想要的一定是Car而不是Plane?因此,具体代理哪个对象的控制权一定是在用户手上而不是我们将其写死。

基于上述的弊端和不足,我们继续来优化MyProxy。

 

改进2:动态根据用户的需求来对方法进行增强

在我们的设计中,始终有个问题没有得以解决,那就是对方法的增强一直是写死的。从最开始的静态代理,我们通过MovableLogProxy、MovableTimeProxy方式来处理,到我们最新的代码MyProxy中依然还是写死的:

也就是说,如果我们想通过MyProxy对方法进行调用前后的计时,那么只能重新编写一个MyTimeProxy。而且我们发现,即使我们重新编写了MyTimeProxy,其实整个类中要改动的地方仅仅是我在图片中用红色圈起来的这一小部分。

基于面向对象的封装思想,这段代码是这个类中变化的地方,我们是可以将它抽出来的。既然确定了目标,下一步就要考虑怎么将它封装起来。列举几种常用的封装方式,并讨论是否适用于我们的需求:

(1)内部封装:将多次使用到的代码单独封装到一个方法里,避免重复编码。这种方式对于我们来说肯定是不适用的。首先这段代码在MyProxy中没有被多次复用,其次它也无法改变MyProxy中只能给方法进行加日志的增强方式。

(2)通过方法传参:这种方式就会比较灵活了,我们可以提前定义一些方法增强的处理方式,再对每种处理方式定义一个枚举值,我们可以在newProxyInstance(ProcessType type)通过接收这个枚举值来决定使用何种处理方式。这种方式看上去可以符合我们的需求,但是它有一个弊端,那就是我们必须要提前定义好方法增强的方式,但是如果日后需要新的增强方式,我们就必须增加switch case的分支,然后再增加相应的枚举值。更加苦恼的是,做完这些以后我们还要重新对项目进行编译、发布,我想这是程序员最不能接受的。从另一个角度来说,对方法的增强方式可以说是无穷无尽的,我们永远也不知道用户想怎么做!

(3)通过面向接口:上边说了,我们永远也不知道用户想怎么做,那就只能考虑将具体的实现方式交给用户自己来做,我们只要将接收到的来自用户的实现嵌入到我们的实现中即可。虽说是让用户自己来定义实现方式,但也要在一定的规则内进行定义,因此我们要定义一个interface让用户去实现。

基于上面的分析,能确定(3)是最符合我们需求的封装方式。那么现在要考虑的就是如何给用户定义这样一个interface让用户实现?

当前的已知条件:用户通过newProxyInstance(Class clazz)方法传给我们的被代理对象的类型,从而我们就能获取到被代理对象内部的所有方法Method[]。

最终目标:对上边已知条件的方法进行增强,具体的增强方式由用户来实现。

由此可见,我们只需要让用户来实现对方法的增强形式即可,那么我们按照这个思路来定义相应的interface:

public interface InvocationHandler {
	
    void invoke();
}

相应的,newProxyInstance也要传入这个接口:

public class MyProxy{

    public static Object newProxyInstance(Class clazz, InvocationHandler h){
        
        ...
    }
}

模拟用户实现InvocationHandler,并且希望对方法的增强方式是在方法调用的前后加上日志:

public class LogHandler implements InvocationHandler{

    @Override
    public void invoke(){
        System.out.println("logger is start...");

        System.out.println("logger is end...");
    }
}

写到这里,我想大家应该发现问题了。之前我们将处理方式写死时是这样的:

在日志的start和end之间,我们通过字符串的拼接完成了方法的调用。现在我们虽然可以通过用户传给我们的invocationHandler获取到用户的实现方式,但是却存在两个问题:

  1. 我们并不知道用户希望将这些增强处理放置在方法调用的何处,在方法调用之前?之后?或是将方法调用穿插在中间?
  2. 即使我们通过一些方式(比如让用户传枚举值)知道了用户想放置在何处,对于前置、后置还好,但是对于例子中这种穿插在中间的情况,我们依然无法处理。因为我们只能通过invocationHandler.invoke获取到用户的所有处理方式,但是无法将方法调用穿插进用户定义好的实现方式中。除此之外,如果用户希望在方法调用出现异常时做一些针对性的处理,我们也是无法实现的。

既然这条路走不通,我们就要换个思路来实现了,重新看一下我们的已知条件:用户通过newProxyInstance(Class clazz)方法传给我们的被代理对象的类型,从而我们就能获取到被代理对象内部的所有方法Method[]。

在实现MyProxy的过程中,我们通过for循环遍历了Method[ ],每一次循环出的Method就是clazz的一个方法,那么我们是否可以把每次循环出来的Method都传给用户,让用户自己进行Method的调用呢?另外我们知道,用户定义该增强方式是希望适用于代理对象的任何一个方法上,因此用户即使每次拿到的不同Method也不用重复编写不同的处理方式,只需要编写一次就可以在所有的方法上复用。

修改InvocationHandler:

package cn.rain.design.proxy.demo2.handler;

import java.lang.reflect.Method;

public interface InvocationHandler {
	
    void invoke(Method method);
}

实现InvocationHandler:

public class LogHandler implements InvocationHandler{

    @Override
    public void invoke(Method method){
        System.out.println("logger is start...");
        method.invoke(???);
        System.out.println("logger is end...");
    }
}

此时又有一个问题,method.invoke(Object o)有一个必须的参数o,我们知道想通过反射进行方法调用的话必须要指明调用此方法的对象是谁。这点很好理解,我们通过反射获取到的Method只是一些字节码,它是没有状态的,比如下面这段代码:

public class Person{

    private String name;
    
    public Person(String name){
        this.name = name;
    }
    
    public void whoAmI(){
        System.out.println("I am " + name);
    }
}

如果通过反射获取到whoAmI这个Method时,简单来说它仅仅是通过Person.class获取到的一段字符串,它并不存在状态,我们并不知道"I am ???"。只有当我们创建Person的实例后 new Person("xiao ming");这段字节码才有了状态,这个方法的调用才有意义。所以说方法的调用必须是以对象为基础的。

好了说回正题,那我们的method.invoke(???)应该传入什么对象呢?这一步的前后是对方法进行增强,是对什么方法增强呢?当然是被代理对象的方法了。因此很好理解,这个地方传入的对象肯定是被代理对象。

那么被代理对象在哪呢?我们回想一下在改进1中我们是怎样创建被代理对象的:

我们之前在MyProxy中直接写死了一个被代理对象new Car(),在改进1的最后总结时也提到了这个不合理的地方,我们虽然知道用户要代理的类型是Movable,但是Movable的实现可以有很多,我们怎么知道用户要代理的具体对象是Car而不是Plane呢?因此这个地方肯定不能由我们来写死,而是要交给用户处理。用户需要告诉我们他要代理的对象究竟是Car还是Plane,所以我们需要在用户创建他的InvocationHandler实例时要求用户传入被代理的对象,因此实现InvocationHandler应该是这样:

package cn.rain.design.proxy.demo2.handler;

import java.lang.reflect.Method;

public class LogHandler implements InvocationHandler{

    private Object target;

    // 用户在创建实例时要传入被代理的对象
    public LogHandler(Object target){
        this.target = target;
    }

    @Override
    public void invoke(Method method){
        // 在被代理对象的方法调用前进行增强
        System.out.println("logger is start...");
        // 针对被代理对象的方法进行调用
        try {
            method.invoke(target);
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 在被代理对象的方法调用后进行增强
        System.out.println("logger is end...");
    }
}

MyProxy的实现也做相应的修改,涉及到的修改会在代码注释中进行说明

package cn.rain.design.proxy.demo2;

import cn.rain.design.proxy.demo2.handler.InvocationHandler;

import java.io.File;
import java.io.FileWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public class MyProxy {

    public static Object newProxyInstance(Class clazz, InvocationHandler h) throws Exception {
        // 换行符
        String rt = "\r\n";

        String methodStr = "";
	    
        // 获取clazz中的所有方法
        Method[] methods = clazz.getMethods();
	for (Method method : methods) {
		methodStr += "	@Override" + rt +
			     "	public void " + method.getName() + "() {"  + rt +
                             // 处理反射获取方法时抛出的异常
			     "	    try{" + rt + 
                             // 循环遍历clazz的所有方法,每次获取到的Method都传回给用户,用于其在自定义方法增强时通过反射进行的调用方法
                             "          Method m = " + clazz.getName() + ".class.getMethod(\"" +method.getName()+ "\");" + rt +
                             // 将此次循环获取到的method对象传回给用户的invocationHanler对象
                             "	       	h.invoke(m);" + rt + 
			     "		} catch (Exception e) {" + rt +
		             "			e.printStackTrace();" + rt + 
			     "		}" + rt + 
                             "	}" + rt + rt ;
		}


	String src =
            "package cn.rain.design.proxy.demo2;" + rt + rt +
            // 引入jdk的Method类
            "import java.lang.reflect.Method;" + rt +
            "import cn.rain.design.proxy.demo2.handler.InvocationHandler;" + rt + rt +
             
            // 这里将要写出的.java文件不再使用TempProxy,而是命名为$Proxy
            "public class $Proxy implements " + clazz.getName() + "{" + rt + rt +
            // 构造此代理对象时,无需再传入被代理对象,因为被代理对象已经传给了用户实现的invocationHandler,我们只需要让用户将它传给我们即可
            "   private InvocationHandler h;" + rt + rt +
            "   public $Proxy(InvocationHandler h) {" + rt +
            "       this.h = h;" + rt  +
            "   }" + rt + rt +
            methodStr + 
            "}" ;

	
	String projectPath = System.getProperty("user.dir");
	// 名称更改为$Proxy
	String filePath = projectPath + "/src/main/java/cn/rain/design/proxy/demo2/$Proxy.java";
 
	File file = new File(filePath);
	FileWriter writer = new FileWriter(file);
	writer.write(src);
	writer.flush();
	writer.close();
 
	JavaCompiler javaCompiler = ToolProvider.getSystemJavaCompiler();

	StandardJavaFileManager fileManager = javaCompiler.getStandardFileManager(null, null, null);
 
	Iterable<? extends JavaFileObject> targetFiles = fileManager.getJavaFileObjects(filePath);
		
	JavaCompiler.CompilationTask task = javaCompiler.getTask(null, fileManager, null, null, null, targetFiles);
	task.call();
	fileManager.close();
 
	URL[] urlArr = new URL[]{new URL("file:/" + System.getProperty("user.dir") + "/src")};
	URLClassLoader urlClassLoader = new URLClassLoader(urlArr);
	Class<?> loadClazz = null;
	try {
		loadClazz = urlClassLoader.loadClass("cn.rain.design.proxy.demo2.$Proxy");
	} catch (ClassNotFoundException e) {
		e.printStackTrace();
	}finally {
		urlClassLoader.close();
	}

	/*
         * 获取代理对象$Proxy的构造器。
         * 由于我们拼接的代理对象字符串的构造器的参数类型不再是被代理对象的Class类型,而是InvocationHandler,因此这里也做相应修改。
         */
	Constructor<?> constructor = loadClazz.getConstructor(InvocationHandler.class);    
        // 通过反射创建代理对象$Proxy的实例,构造器的参数是InvocationHandler,即用户传给我们的h
	return constructor.newInstance(h);
	}
}

对代码进行测试:

package cn.rain.design.proxy.demo2;

import cn.rain.design.proxy.demo1.model.Car;
import cn.rain.design.proxy.demo1.model.Movable;
import cn.rain.design.proxy.demo2.handler.LogHandler;

public class Main {
    public static void main(String[] args) throws Exception {
        // 创建被代理对象的实例
        Car car = new Car();
        // 创建自定义的方法处理方式的实例
        LogHandler h = new LogHandler(car);
        // 获取代理对象
        Movable m = (Movable)MyProxy.newProxyInstance(car.getClass().getInterfaces()[0], h);
        m.move();
    }
}

这里需要注意的是第一个参数传递的是代理对象所实现接口的类型,而不是传递car.getClass()。至于为什么这么做,我们留在最后一篇文章进行说明。

运行后发现已经生成了$Proxy.java和编译成功的$Proxy.class,我们看一下$Proxy.java的内容:

大家有没有发现它其实和我们最初写的静态代理十分相似,唯一的区别的就是引用变量变成了InvocationHandler,这是因为我们将方法的增强方式抽出来让用户去实现了。

我们再重新梳理一下整个流程:

  1. 我们通过调用MyProxy.newProxyInstance(car.getClass().getInterfaces()[0], h)生成了Car的代理对象,这个Car我们是传递给了h。第一个参数决定了$Proxy这个代理对象具体要去实现哪个接口,进一步可以通过反射获取该接口中的所有方法;第二个参数决定了要对原有(被代理对象)的方法进行怎样的增强。
  2. 获取到两个参数后,我们通过字符串拼接、IO的方式在磁盘上生成了一个类似于我们前边说的静态代理的$Proxy.java文件。
  3. 对$Proxy.java.java进行编译,在磁盘上生成其字节码文件$Proxy.class。
  4. 通过类加载机制,将磁盘上的$Proxy.class加载进内存。
  5. 通过反射,将$Proxy.class实例化出的代理对象返回给用户。
  6. 用户调用代理对象的方法即调用上面图中的方法,比如当用户调用move方法时,方法内部先利用反射获取到该方法的Method对象,再将m传递给用户自定义实现的invoke方法。
  7. 用户实现的invoke方法拿到Method对象以后,由于LogHandler中接收了被代理对象target,因此可以通过反射对这个Method进行调用method.invoke(target),并且在调用前后,可以自由的对方法进行增强。

其实抛开InvocationHandler不谈的话,动态代理最终做的事情和静态代理完全一样,就是在原有(被代理对象)的方法调用基础上进行一些方法增强。而所谓的“动态”,就是我们无需再去大量编写生成静态代理对象所需的.java文件(如第(2)篇文章中的MovableLogProxy.java、MovableTimeProxy.java),便可以为我们”动态“(通过字符串->IO->编译->classLoader->反射)的生成代理对象。

所以说,如果你明白了什么是静态代理,那你就明白了什么是动态代理,代理无非就是在原有方法调用的基础上进行增强:前置增强、后置增强、环绕增强、原有方法调用发生异常时。而动态代理,无非是在代理的基础上更加灵活。现在想想所谓“最难的设计模式”是不是也没那么难,甚至它的概念相当简单,它的“难”是难在如何设计“动态”上。

到这里,我们自己实现动态代理的过程基本结束了,其间有一些问题可能说的比较啰嗦,因为我希望任何人在看我这篇文章的过程中都不用因为某些知识点去翻其他文章,这样很容易打断思路,至少我就是这样。在下一篇文章中,我会比较一下jdk原生的动态代理和我们实现的还有哪些区别,并对这些区别进行一些说明。但是我不会带大家分析它的源码,因为源码分析网上有很多,有兴趣可以自己去翻一翻,我就不在这里重复造轮子了。另外我想说明的是,我们的实现方式是参考自jdk,但是具体细节肯定存在差别,比如用什么方式将class加载进内存的、如何进行反射调用的、一些更细致的逻辑判断等等。但是我想这些并不是我们主要关心的。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值