查看原文
其他

手动模拟JDK动态代理

点击上方 "程序员小乐"关注公众号, 星标或置顶一起成长

每天凌晨00点00分, 第一时间与你相约

每日英文

Everything will pass eventually, but at times one's affected mood may linger. Just adjust your mood, and the world will look completely different.

没有过不去的事情,只有过不去的心情。只要把心情变一变,世界就完全不一样了。

每日掏心话

如果你越来越冷漠,你以为你成长了,但其实没有。长大应该是变温柔,对全世界都温柔。


来自:赐我白日梦 | 责编:乐乐

链接:cnblogs.com/ZhuChangwu/p/11648911.html

程序员小乐(ID:study_tech)第 664 次推文   图片来自网络


往日回顾:细思极恐!你真的会写 Java 吗?建议收藏



   正文   

  

为哪些方法代理?

实现自己动态代理,首先需要关注的点就是,代理对象需要为哪些方法代理? 原生JDK的动态代理的实现是往上抽象出一层接口,让目标对象和代理对象都实现这个接口,怎么把接口的信息告诉jdk原生的动态代理呢? 如下代码所示, Proxy.newProxyInstance()方法的第二个参数将接口的信息传递了进去第一个参数的传递进去一个类加载器,在jdk的底层用它对比对象是否是同一个,标准就是相同对象的类加载器是同一个

ServiceInterface) Proxy.newProxyInstance(service.getClass().getClassLoader()
                , new Class[]{ServiceInterface.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                System.out.println("前置通知");
                method.invoke(finalService,args);
                System.out.println("后置通知");
                return proxy;
            }
        });

我们也效仿它的做法. 代码如下:

public class Test {
    public static void main(String[] args) {
        IndexDao indexDao = new IndexDao();
        Dao  dao =(Dao) ProxyUtil.newInstance(Dao.class,new MyInvocationHandlerImpl(indexDao));
        assert dao != null;
        System.out.println(dao.say("changwu"));
    }
}


拿到了接口的Class对象后,通过反射就得知了接口中有哪些方法描述对象Method,获取到的所有的方法,这些方法就是我们需要增强的方法


如何将增强的逻辑动态的传递进来呢?


JDK的做法是通过InvocationHandler的第三个参数完成,他是个接口,里面只有一个抽象方法如下: 可以看到它里面有三个入参,分别是 代理对象,被代理对象的方法,被代理对象的方法的参数

public Object invoke(Object proxy, Method method, Object[] args)
        throws Throwable;
}

当我们使用jdk的动态代理时,就是通过这个重写这个钩子函数,将逻辑动态的传递进去,并且可以选择在适当的地方让目标方法执行

InvocationHandler接口必须存在必要性1:

为什么不传递进去Method,而是传递进去InvocationHandler对象呢? 很显然,我们的初衷是借助ProxyUtil工具类完成对代理对象的拼串封装,然后让这个代理对象去执行method.invoke(), 然而事与愿违,传递进来的Method对象的确可以被ProxyUtil使用,调用method.invoke()但是我们的代理对象不能使用它,因为代理对象在这个ProxyUtil还以一堆等待拼接字符串, ProxyUtil的作用只能是往代理对象上叠加字符串,却不能直接传递给它一个对象,所以只能传递一个对象进来,然后通过反射获取到这个对象的实例,继而有可能实现method.invoke()

InvocationHandler接口必须存在必要性2:

通过这个接口的规范,我们可以直接得知回调方法的名字就是invoke()所以说,在拼接字符串完成对代理对象的拼接时,可以直接写死它

思路

我们需要通过上面的ProxyUtil.newInstance(Dao.class,new MyInvocationHandlerImpl(indexDao))方法完成如下几件事

  • 根据入参位置的信息,提取我们需要的信息,如包名,方法名,等等

  • 根据我们提取的信息通过字符串的拼接完成一个全新的java的拼接

    • 这个java类就是我们的代理对象

  • 拼接好的java类是一个String字符串,我们将它写入磁盘取名XXX.java

  • 通过ProxyUtil使用类加载器,将XXX.java读取JVM中,形成Class对象

  • 通过Class对象反射出我们需要的代理对象

ProxyUtil的实现如下:


public static Object newInstance(Class targetInf, MyInvocationHandler invocationHandler) {

    Method methods[] = targetInf.getDeclaredMethods();
    String line = "\n";
    String tab = "\t";
    String infName = targetInf.getSimpleName();
    String content = "";
    String packageContent = "package com.myproxy;" + line;
    //   导包,全部导入接口层面,换成具体的实现类就会报错
    //   
    String importContent = "import " + targetInf.getName() + ";" + line
                           + "import com.changwu.代理技术.模拟jdk实现动态代理.MyInvocationHandler;" + line
                           + "import java.lang.reflect.Method;" + line
                           + "import java.lang.Exception;" + line;

    String clazzFirstLineContent = "public class $Proxy implements " + infName +"{"+ line;
    String filedContent = tab + "private MyInvocationHandler handler;"+ line;
    String constructorContent = tab + "public $Proxy (MyInvocationHandler  handler){" + line
            + tab + tab + "this.handler =handler;"
            + line + tab + "}" + line;
    String methodContent = "";
    // 遍历它的全部方法,接口出现的全部方法进行增强
    for (Method method : methods) {
        String returnTypeName = method.getReturnType().getSimpleName();         method.getReturnType().getSimpleName());

        String methodName = method.getName();
        Class<?>[] parameterTypes = method.getParameterTypes();

        // 参数的.class
        String paramsClass = "";
        for (Class<?> parameterType : parameterTypes) {
            paramsClass+= parameterType.getName()+",";
        }

        String[] split = paramsClass.split(",");

        //方法参数的类型数组 Sting.class String.class
        String argsContent = "";
        String paramsContent = "";
        int flag = 0;
        for (Class arg : parameterTypes) {
            // 获取方法名
            String temp = arg.getSimpleName();
            argsContent += temp + " p" + flag + ",";
            paramsContent += "p" + flag + ",";
            flag++;
        }
        // 去掉方法参数中最后面多出来的,
        if (argsContent.length() > 0) {
            argsContent = argsContent.substring(0, argsContent.lastIndexOf(",") - 1);
            paramsContent = paramsContent.substring(0, paramsContent.lastIndexOf(",") - 1);
        }
        methodContent += tab + "public " + returnTypeName + " " + methodName + "(" + argsContent + ") {" + line
                + tab + tab+"Method method = null;"+line
                + tab + tab+"String [] args0 = null;"+line
                + tab + tab+"Class<?> [] args1= null;"+line

                // invoke入参是Method对象,而不是上面的字符串,所以的得通过反射创建出Method对象
                + tab + tab+"try{"+line
                // 反射得到参数的类型数组
                 + tab + tab + tab + "args0 = \""+paramsClass+"\".split(\",\");"+line
                 + tab + tab + tab + "args1 = new Class[args0.length];"+line
                 + tab + tab + tab + "for (int i=0;i<args0.length;i++) {"+line
                 + tab + tab + tab + "   args1[i]=Class.forName(args0[i]);"+line
                 + tab + tab + tab + "}"+line
                // 反射目标方法
                + tab + tab + tab + "method = Class.forName(\""+targetInf.getName()+"\").getDeclaredMethod(\""+methodName+"\",args1);"+line
                + tab + tab+"}catch (Exception e){"+line
                + tab + tab+ tab+"e.printStackTrace();"+line
                + tab + tab+"}"+line
                + tab + tab + "return ("+returnTypeName+") this.handler.invoke(method,\"暂时不知道的方法\");" + line; //
                 methodContent+= tab + "}"+line;
    }

    content = packageContent + importContent + clazzFirstLineContent + filedContent + constructorContent + methodContent + "}";

    File file = new File("d:\\com\\myproxy\\$Proxy.java");
    try {
        if (!file.exists()) {
            file.createNewFile();
        }

        FileWriter fw = new FileWriter(file);
        fw.write(content);
        fw.flush();
        fw.close();

        // 将生成的.java的文件编译成 .class文件
        JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
        StandardJavaFileManager fileMgr = compiler.getStandardFileManager(nullnullnull);
        Iterable units = fileMgr.getJavaFileObjects(file);
        JavaCompiler.CompilationTask t = compiler.getTask(null, fileMgr, nullnullnull, units);
        t.call();
        fileMgr.close();

        // 使用类加载器将.class文件加载进jvm
        // 因为产生的.class不在我们的工程当中
        URL[] urls = new URL[]{new URL("file:D:\\\\")};
        URLClassLoader urlClassLoader = new URLClassLoader(urls);
        Class clazz = urlClassLoader.loadClass("com.myproxy.$Proxy");
        return clazz.getConstructor(MyInvocationHandler.class).newInstance(invocationHandler);
    } catch (Exception e) {
        e.printStackTrace();
    }
       return null;
}
}

运行的效果:

package com.myproxy;
import com.changwu.myproxy.pro.Dao;
import com.changwu.myproxy.pro.MyInvocationHandler;
import java.lang.reflect.Method;
import java.lang.Exception;
public class $Proxy implements Dao{
    private MyInvocationHandler handler;
    public $Proxy (MyInvocationHandler  handler){
        this.handler =handler;
    }
    public String say(String p) {
        Method method = null;
        String [] args0 = null;
        Class<?> [] args1= null;
        try{
            args0 = "java.lang.String,".split(",");
            args1 = new Class[args0.length];
            for (int i=0;i<args0.length;i++) {
               args1[i]=Class.forName(args0[i]);
            }
            method = Class.forName("com.changwu.myproxy.pro.Dao").getDeclaredMethod("say",args1);
        }catch (Exception e){
            e.printStackTrace();
        }
        return (String) this.handler.invoke(method,"暂时不知道的方法");
    }
}


解读

通过newInstance()用户获取到的代理对象就像上面的代理一样,这个过程是在java代码运行时生成的,但是直接看他的结果和静态代理差不错,这时用户再去调用代理对象的say(), 实际上就是在执行用户传递进去的InvocationHandeler里面的invoke方法, 但是亮点是我们把目标方法的描述对象Method同时给他传递进去了,让用户可以执行目标方法+增强的逻辑

当通过反射区执行Method对象的invoke()方法时,指定的哪个对象的当前方法呢? 这个参数其实是我们手动传递进去的代理对象代码如下

public class MyInvocationHandlerImpl implements MyInvocationHandler {
    private Object obj;
    public MyInvocationHandlerImpl(Object obj) {
        this.obj = obj;
    }
    @Override
    public Object invoke(Method method, Object[] args) {
        System.out.println("前置通知");
        try {
            method.invoke(obj,args);
        } catch (Exception e) {
            e.printStackTrace();
        }  
        System.out.println("后置通知");
        return null;
    }
}


欢迎在留言区留下你的观点,一起讨论提高。如果今天的文章让你有新的启发,学习能力的提升上有新的认识,欢迎转发分享给更多人。

欢迎各位读者加入程序员小乐技术群,在公众号后台回复“加群”或者“学习”即可。

猜你还想看


阿里、腾讯、百度、华为、京东最新面试题汇集

终于明白为什么要加 final 关键字了!

JVM 发生内存溢出的 8 种原因、及解决办法

一起来学Java注解(Annotation),看了都说好!


关注微信公众号「程序员小乐」,收看更多精彩内容
嘿,你在看吗?

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存