欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

JDK的动态代理(通俗易懂)

程序员文章站 2022-07-10 18:54:12
读完本篇文章将会了解以下问题1.JDK的动态代理的整体流程2.代理对象帮我们做了什么3.为什么JDK的动态代理是基于接口的代理(继承为什么不行)4.生成代理实例化对象方法Proxy.newProxyInstance()的三个参数的作用分别是什么--------------------------------------------------------------------------------------------------------------------------...

读完本篇文章将会了解以下问题

1.JDK的动态代理的整体流程

2.代理对象帮我们做了什么

3.为什么JDK的动态代理是基于接口的代理(继承为什么不行)

4.生成代理实例化对象方法Proxy.newProxyInstance()的三个参数的作用分别是什么

---------------------------------------------------------------------------------------------------------------------------

在读此篇博文前需要了解:

java类加载机制的原理和作用

java反射的原理和基本用法

1.JDK的动态代理的整体流程

举个例子,网红giao哥会唱会跳(把唱和跳理解为两个技能,不论是谁,只要这个人有唱的技能就可以唱,有跳的技能就可以跳)火了,随即他想办一场演唱会,所以找了一个经纪人。原本一切顺利,但演唱会那天giao哥突然肚子疼,上不了场,但演唱会还得继续开,经济人就头疼了,因为经纪人不会唱跳(没有唱、跳这两个技能)。所以经纪人找到了giao哥的弟弟,这位弟弟拥有giao哥的唱和跳的技能书。随后经纪人从弟弟那里获取到了giao哥唱和跳的技能书,在幕后用高科技设备把获取到的技能书转化成了giao哥去唱歌,最终解决了这个问题。

上述例子最后部分有点牵强,因为java拥有反射机制,可以用method.invoke(obj,args)方法来调用obj对象的method方法。

上述例子中对应下文的java名词

Giao哥:被代理对象

经纪人:方法增强类

弟弟:代理对象

唱和跳的技能:被代理对象所实现接口中的方法

唱和跳的技能书:保存在方法区中的模板信息

高科技设备把获取到的技能书转化成了giao哥去唱歌:利用反射调用目标方法

接下来我们用代码来复现这一过程:

Star明星接口:有唱和跳两个方法定义

package user;

public interface Star {

    String sing();

    String dance();
} 

Giao哥类:实现自Star接口,重写接口内唱跳方法

package user;

public class BrotherGiao implements Star{

    @Override
    public String sing() {

        System.out.println("Giao哥:一给我里giaogiao");
        return "giao哥唱完了" ;
    }

    @Override
    public String dance( ) {

        System.out.println("Giao哥:边唱边giao");
        return "giao哥跳完了" ;
    }
} 

Agent 经纪人类:实现自InvocationHandler接口,内部保存被代理对象,有invoke和creatProxy方法我们下面说

package user;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class Agent implements InvocationHandler {
    //目标类,也就是被代理对象
    private Object object;

    public void setTarget(Object target)
    {
        this.object = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {
        // 这里可以做增强
        System.out.println("经纪人:唱歌前帮giao收钱");
        Object result = method.invoke(object, args);
        System.out.println("经纪人:唱歌后帮giao打扫战场");

        return result;
    }

    // 生成代理类
    public Object creatProxy()
    {
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
    }
} 

一个普通的主函数启动类

package run;

import user.Agent;
import user.BrotherGiao;
import user.Star;

public class Practice001 {
    public static void main(String args[]){
        //将动态代理生成的.class文件持久化到磁盘上,老版本JDK用下面这个命令
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
        //新版本JDK用下面这个命令
        //System.getProperties().put("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true");
        //实例化giao哥对象
        BrotherGiao brotherGiao = new BrotherGiao();
        //实例化经纪人对象
        Agent agent = new Agent();
        //将giao哥传入经纪人对象内
        agent.setTarget(brotherGiao);
        //生成代理类并实例化对象
        Object obj = agent.creatProxy();
        //多态,因继承自Star接口,所以可用该接口来接收
        Star star = (Star) obj;
        System.out.println(star.sing());
    }  
} 

运行结果:

JDK的动态代理(通俗易懂)

我们看一下主函数里的几行代码,先创建Giao哥对象,再去创建经纪人对象,将Giao哥传入经纪人对象内,经纪人调用createProxy方法生成代理类实例(对应上述例子找弟弟),将代理类实例利用java多态性质用Star接口接收,最后用star.sing()调用方法(接口.方法最终为调用为接口实现类.方法),代码很简单,就不赘余了。

有些小伙伴可能会问,为什么经纪人不直接帮giao哥上去唱啊,还去找弟弟多麻烦。我们看Giao哥类实现Star接口,而经纪人类实现InvocationHandler接口,经纪人类内部根本就没有Star接口内唱和跳的方法(对应上述例子,经纪人不会唱跳),所以经纪人无法直接帮Giao哥去唱,那么jdk动态代理问题的重点就变成了:

1:如何找到一个与被代理类拥有相同方法的代理类并将目标方法的参数传递回增强类,增强类如果可以拿到代理对象的方法、参数,同时其内部还保存着被代理对象的实例,那么就可以通过反射来调用该方法,从而实现代理。

2:动态代理的目的是为了给方法做增强,让方法更灵活,那么假设我们真的找到了一个与被代理类拥有相同方法的类,该如何与增强类建立联系(代理对象调用目标方法的时候怎么能调回增强类中的invoke方法)

想弄清这两个问题就必须跟源码,我们重点来看这两行代码

//目标类,也就是被代理对象
private Object object;
public void setTarget(Object target)
{
   this.object = target;
}
public Object creatProxy()
{
        return Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), this);
}

可以看到newProxyInstance方法主要有三个参数,第一个参数为被代理对象的类加载器,第二个参数为被代理对象所实现的接口,第三个this为主方法中的agent对象,因为是agent调用的creatProxy方法。然后我们跟进newProxyInstance方法的源码,前两个参数都很好理解,一个是被代理类的类加载器,一个是被代理类实现的接口数组,重点看第三个参数,InvocationHandler  h接口,记住他下面有用。我们传入的是增强类的对象,而增强类恰恰是实现了InvocationHandler这个接口的,那么我们大胆猜测,上述的第2个问题是不是通过这个InvocationHandler h来关联代理类和增强类的呢?

 public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
        throws IllegalArgumentException
    {
        //检验h不为空,h为空抛异常
        Objects.requireNonNull(h);
        //接口的类对象拷贝一份
        final Class<?>[] intfs = interfaces.clone();
        //进行安全性检查
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }

        /*
         * Look up or generate the designated proxy class.
         *  查询(在缓存中已经有)或生成指定的代理类的class对象。
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        /*
         * Invoke its constructor with the designated invocation handler.
         * 使用指定的调用处理程序调用其构造函数
         */
        try {
            if (sm != null) {
                checkNewProxyPermission(Reflection.getCallerClass(), cl);
            }
            //得到代理类对象的构造函数,这个构造函数的参数由constructorParams指定
            final Constructor<?> cons = cl.getConstructor(constructorParams);
            final InvocationHandler ih = h;
            if (!Modifier.isPublic(cl.getModifiers())) {
                AccessController.doPrivileged(new PrivilegedAction<Void>() {
                    public Void run() {
                        cons.setAccessible(true);
                        return null;
                    }
                });
            }
            //这里生成代理对象并返回
            return cons.newInstance(new Object[]{h});
        } catch (IllegalAccessException|InstantiationException e) {
            throw new InternalError(e.toString(), e);
        } catch (InvocationTargetException e) {
            Throwable t = e.getCause();
            if (t instanceof RuntimeException) {
                throw (RuntimeException) t;
            } else {
                throw new InternalError(t.toString(), t);
            }
        } catch (NoSuchMethodException e) {
            throw new InternalError(e.toString(), e);
        }
    } 

我们看到有生成指定的代理类的class对象这一步:Class<?> cl = getProxyClass0(loader, intfs);主函数中有这样一个方法,可以将动态代理生成的.class文件持久化到磁盘上

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");

JDK的动态代理(通俗易懂)

public final class $Proxy0 extends Proxy implements Star {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m4;
    private static Method m0;

    public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
    }

    public final boolean equals(Object var1) throws  {
        try {
            return (Boolean)super.h.invoke(this, m1, new Object[]{var1});
        } catch (RuntimeException | Error var3) {
            throw var3;
        } catch (Throwable var4) {
            throw new UndeclaredThrowableException(var4);
        }
    }

    public final String dance() throws  {
        try {
            return (String)super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String toString() throws  {
        try {
            return (String)super.h.invoke(this, m2, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final String sing() throws  {
        try {
            return (String)super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    public final int hashCode() throws  {
        try {
            return (Integer)super.h.invoke(this, m0, (Object[])null);
        } catch (RuntimeException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("user.Star").getMethod("dance");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m4 = Class.forName("user.Star").getMethod("sing");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
} 

我们打开这个$Proxy0.class文件,发现其继承自Proxy类,这也就解释了为什么JDK的动态代理是基于接口的代理,因为java为单继承。然后我们看到这个类实现了Star接口,那我们再大胆猜测一下,这个类中是不是也实现了Star接口中的sing()方法呢?往下一看还真的是,这也就解释了问题1:java不是去找一个与被代理类拥有相同方法的代理类,而是直接创建一个和被代理类实现相同接口并且拥有相同方法的代理类。我们接着看到这个代理类的构造方法传入InvocationHandler然后利用父类构造 ,接下来再看sing()方法,返回值为:return (String)super.h.invoke(this, m4, (Object[])null); super为调用父类,父类的h我们在上边让各位小伙伴重点记忆了一下,h为增强类的实例对象,也就是我们在调用newProxyInstance方法时的第三个参数 this。这下全理清楚了,super.h.invoke调用的就是增强类中的invoke方法,这也就解释了问题2:代理对象调用目标方法的时候怎么能调回增强类中的invoke方法。然后我们再看参数,this就是当前代理类对象,m4为sing方法,Object[])null为sing的方法参数。最终再看回增强类中的invoke方法

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
    {
        //这里可以做增强
        System.out.println("经纪人:唱歌前帮giao收钱");

        Object result = method.invoke(object, args);

        System.out.println("经纪人:唱歌后帮giao打扫战场");

        return result;
    }

参数一致,至此整个JDK动态代理过程结束,我们可以在增强类中去自定义我们的逻辑了。

实验类类图关系:

JDK的动态代理(通俗易懂)

总结:

  • Jdk动态代理是由Java内部的反射机制来实现的,目标类基于统一接口InvocationHandler。
  • 代理对象是在程序运行时产生;
  • 对代理对象的所有接口方法调用都会转发到InvocationHandler.invoke()方法,在invoke()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;
  • 对于从Object中继承的方法,JDK动态代理会把hashCode()、equals()、toString()这三个非接口方法转发给InvocationHandler,其余的Object方法则不会转发。

本文地址:https://blog.csdn.net/qq_36756682/article/details/108256324