手动模拟JDK动态代理
发布日期:2021-05-09 00:40:28 浏览次数:14 分类:博客文章

本文共 6071 字,大约阅读时间需要 20 分钟。

为哪些方法代理?

实现自己动态代理,首先需要关注的点就是,代理对象需要为哪些方法代理? 原生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

运行的效果:

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

解读

通过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;    }}
上一篇:深入理解 Spring 环境初始化
下一篇:mybatis - 通用mapper

发表评论

最新留言

做的很好,不错不错
[***.243.131.199]2025年04月14日 02时01分11秒