JDK动态代理的笔记与学习思路

        三年CRUD下来,对于一些知识点的印象已经模糊,作为一个有梦想的CV战士,最近决定重操旧业(刷题面试),在这里记录下复习JDK动态代理(以下统称动态代理)时的笔记。

JDK动态代理是什么

        说到动态代理,第一反应肯定是代理模式,GOF23(23种设计模式)之一,动态代理的目的就是让代码自己生成代理对象,从而解放我们的双手,让我们只需要关心类本身和代理类的增强方法,而不需要再关心代理类和代理对象的创建过程。

动态代理的实现方式

        关于动态代理的实现,主要是一个接口(InvocationHandler)和一个类(Proxy)

        首选需要有一个类来实现InvocationHandler接口。实现这个接口的类可以来进行对被代理对象进行代理增强操作

InvocationHandler接口

        关于这个接口,里面只有一个方法,就是invoke方法,看到这个方法中有三个参数,第一个是Object,第二个是Method,第三个是Object[] args

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

        看到这是不是有点楞逼?三个参数最眼熟的应该是Method吧?对反射有所了解的应该知道,Method就是方法的意思,那另外两个是什么呢?不要急,我们点开这个接口的源码。大家可以看到,在源码中对这三个参数的解释

        这三个参数可以先根据注释翻译一下,再打个标记,等下去验证一番

标记一:重写invoke方法的三个参数到底指什么

 * @param   proxy the proxy instance that the method was invoked on

        这行英文翻译过来,意思就是——对其调用方法的代理实例

        这里应该已经明了了,这个就是动态代理自动生成出来的代理对象

        再来看第二个参数

 * @param   method the {@code Method} instance corresponding to
     * the interface method invoked on the proxy instance. 

        翻译一下就是——代理实例调用的,实例对应的接口方法

        这个大致意思代理对象实现的接口的方法

        再来看第三个参数

* @param   args an array of objects containing the values of the
     * arguments passed in the method invocation on the proxy instance,
     * or {@code null} if interface method takes no arguments.

        翻译一下——大致就是方法执行时的参数

实现InvocationHandler接口的类

        知道这个接口的需要实现的方法后我们来写个类实现下这个接口

 

package proxyPo;

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

/**
 * 这里实现了InvocationHandler接口
 */
public class UserProxy implements InvocationHandler {

    private Object target;

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

    public UserProxy(Object target) {
        this.target = target;
    }

    /**
     * 该invoke方法是执行的代理增强方法
     * @param proxy
     * @param method
     * @param args
     * @return
     * @throws Throwable
     */
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        System.out.println("这里执行了"+proxy.getClass().getName()+"对象的"+method.getName()+"方法");
        method.invoke(target,args);
        System.out.println("这里执行结束");

        return null;
    }
}

        首先可以看到我这个类里面有个变量Object target,这个变量就是需要被这个增强类UserProxy代理的接口。

        可以看到我这个UserProxy类实现了InvocationHandler接口,并实现invoke方法,然后我们可以看到我的方法里有个method.invoke(target,args),这个方法眼熟吧?反射执行某个方法是不是?这里就是我们代理对象真正执行被代理对象的方法的位置。我们在执行接口方法前,输出了两行语句,方便等下验证

        代理增强类写完了,接下来可以就需要写一下被代理类和被代理类实现的接口

创建被代理类和接口

User接口

package dao;

public interface User {

    void doSth();
}

UserImpl类实现User接口

package dao;

public class UserImpl implements User{

    public void doSth() {
        System.out.println("这里进行了XX操作");
    }
}

测试代理结果

创建完了被代理对象,我们先写个测试类来验证下结果

/**
 * 这里测试代理对象增加日志
 */
public class ProxyTest {


    /**
     * 这里用传统的方式new出代理对象后用set给代理对象赋予真是对象
     */
    @Test
    public void proxyTest(){
        User user = new UserImpl();
        UserProxy proxy = new UserProxy(user);
        //这里用Proxy的静态方法newProxyInstance创建代理对象实例
        User userProxy = (User)Proxy.newProxyInstance(this.getClass().getClassLoader(),user.getClass().getInterfaces(),proxy);
        userProxy.doSth();
    }
}

运行这个测试类,得到的结果是

这里执行了com.sun.proxy.$Proxy9对象的doSth方法
这里进行了XX操作
这里执行结束

        这里可以看到我们的增强类UserProxy中增加的方法已经被执行,那么让我们来看看这段代码是怎么创建代理类的对象的

        我们可以看到这里调用了Proxy.newProxyInstance方法去创建,那么先看下这个方法的参数,老规矩我们点进方法内部,可以看到这个方法有三个传入参数ClassLoader loader, Class<?>[] interfaces, InvocationHandler h

* @param   loader the class loader to define the proxy class

        这里翻译一下,这个loader是个类加载器

 * @param   interfaces the list of interfaces for the proxy class to implement

        这里翻译一下,interfaces这个参数就是代理对象实现的接口数组

 * @param   h the invocation handler to dispatch method invocations to

        这里翻译一下,h这个参数就是  将方法调用分派到的调用处理程序 ,这个看起来比较拗口, 但是我们看下,h这个参数的类型是InvocationHandler,就是我们刚才的增强类UserProxy实现的接口。

        这样一来,这个创建代理类的方法中三个参数我们都已经知道是什么了。但是你以为这就结束了吗?

        我们来看看这个Proxy创建出来的代理类对象到底是什么呢?让我们来debug一下。

        ???为什么是null?null对象为什么会有执行方法?这里不该抛出空指针吗?

        看来要开始新一轮的探索了,在这里我们打个标记

标记二:为什么代理对象的值是null

Proxy类创建代理对象的流程

        首先让我们来看看Proxy类到底是怎么创建代理对象的

        点进方法后,我们直接跳过最前面那部分参数校验,来看到这个方法

        /*
         * Look up or generate the designated proxy class.
         */
        Class<?> cl = getProxyClass0(loader, intfs);

        老规矩,我们先来翻译一下注释,意思就是——查找或生成指定的代理类

        很明显这就是我们要找的方法了,那继续点进去,发现这里有个参数校验,校验接口数量是否超过65535(真的会有实现这么多接口的类吗。。)

        过了参数校验,就进入了proxyClassCache.get(loader, interfaces)方法。

        看到proxyClassCache这个,首先想到的就是cache缓存,没错,进去后先会在缓存中查找是否已有代理类生成,如果没有再去生成,那我们直接就来看代理类的生成逻辑

        // create subKey and retrieve the possible Supplier<V> stored by that
        // subKey from valuesMap
        Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));

        我们来看这行代码,这里传入了我们的两个参数,构造器和接口,返回了一个非空的object

        我们来找到这个方法的实现类ProxyClassFactory

        我们继续,跳过上面一系列对名字的操作,往下看会看到这么一段代码

            /*
             * Choose a name for the proxy class to generate.
             */
            long num = nextUniqueNumber.getAndIncrement();
            String proxyName = proxyPkg + proxyClassNamePrefix + num;

        这里有个String变量叫做proxyName,看变量名字,是不是就是代理名称?我们再看下proxyClassNamePrefix这个参数,这个参数的值是一个常量"$Proxy",最后的num,国际通用简称。number。那这个参数是不是就很明显了,这是我们代理类的名称,就是我们刚才测试运行结果时打印出来的com.sun.proxy.$Proxy9这个类的类名。

        从这里开始往下看,我们可以看到这行代码

            /*
             * Generate the specified proxy class.
             */
            byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                proxyName, interfaces, accessFlags);
            try {
                return defineClass0(loader, proxyName,
                                    proxyClassFile, 0, proxyClassFile.length);
            }

        这个注释一点都不拗口,就是生成指定的代理类,我们可以看到 ProxyGenerator.generateProxyClass方法传入了三个参数并且返回了一个byte数组,那我们再点进去看看这个方法里面干了什么

        点进去,是不是很眼熟?IO流 ,Files.write(path, classFile);

        看到这里基本就明白了吧,我们的代理类的class就是在这里被存入内存的。

        但是这就结束了吗?之前打的两个标记还没有被解答呢,那我们只好看看代理类里面的代码究竟是啥样的了,可是类是被加载在内存中的,我们怎么看呢?既然源码用了IO流,那么我们也模仿着这一个吧,先写个工具类。

/**
 * 这个类用于生成代理对象的class文件到硬盘中
 */
public class ProxyClassUtil {

    public static void createProxyClass(Class class1) throws IOException {
        //这里是类名
        String className = "ProxyClass";
        //这里是类的加载路径
        String classPath = "E:\\javaProject\\GOF23\\Proxy\\target\\classes\\com\\pc";
        //这里复制底层代码,原实现中有三个参数,但是有重载构造方法,可以支持只传两个参数
        byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
                className, new Class[]{class1});
        OutputStream outputStream = new FileOutputStream(classPath+className+".class");
        outputStream.write(proxyClassFile);
        outputStream.flush();
        outputStream.close();
    }
}

        我们可以看到,这个工具类里,我们也用了ProxyGenerator.generateProxyClass方法来获取byte[],然后输出到class文件内,但是这里我们只传了两个参数,第三个参数在重载时被自动赋值了49,我们来看看第三个参数是什么。

   * @param accessFlags access flags of the proxy class

         可以看到这个参数的注释,是个标记为,OK那我们就先不传,看能不能实现,在我们的测试类中增加代码来创建这个class文件

ProxyClassUtil.createProxyClass(User.class);

        执行完成后可以看到class文件已经被创建

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

import com.pc.dao.User;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class ProxyClass extends Proxy implements User {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public ProxyClass(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 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 void doSth() throws  {
        try {
            super.h.invoke(this, m3, (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"));
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m3 = Class.forName("com.pc.dao.User").getMethod("doSth");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

        最下面有个静态代码块, 在类加载的时候,给m0,m1,m2,m3进行了初始化,我们看右边,是不是就是反射创建的Method类?这里可以看到,equals,toString,doSth,hashCode四个方法都有被创建,那么我们再来回顾一下之前我们留下的标记

标记一:重写invoke方法的三个参数到底指什么

标记二:为什么代理对象的值是null

        我们先来看重写invoke方法的三个参数到底是什么?我们先找到equals方法,这里可以看到,调用equals方法的时候,实际上调用的是

super.h.invoke(this, m1, new Object[]{var1})

        那么这个super.h是什么呢?我们可以看下h的类型,你会发现是InvocationHandler这个接口,也就是说,在调用这个代理类的equals时,实际上时调用了代理类的invoke方法,那我们再来看这个方法的参数。

        第一个参数是this,就是当前对象的引用,就是这个类实现的接口,就是这个User。

        第二个参数是m1,这个m1就是之前在静态代码块初始化的Method变量,实际就是通过反射获取到的被代理类的equals方法 。

        第三个参数是new Object[]{var1},这个val1可以看到就是调用equals方法时的传入参数,再将之封装成object数组。

        然后我们再回来看我们的增强类UserProxy的invoke方法

        是不是发现我们这个方法的返回是null?那么我们来做个试验,看能不能解决这个问题,我们把方法的返回值改为method.invoke中的返回值。

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("这里执行了"+proxy.getClass().getName()+"对象的"+method.getName()+"方法");
        Object result = method.invoke(target,args);
        System.out.println("这里执行结束");
        return result;
    }

        我们再来看下debug出来的对象是什么

         是不是就可以看到我们这个userProxy对象的返回数据了?这个对象是$Proxy9,而打印的出的结果为什么是UserImpl呢?这个其实在上面也已经有了解答,因为代理对象的toString实际上是调用了被代理对象的toString,被代理对象的toString打印出来当然就是被代理对象的信息啦!

        看到这里,对于动态代理的探究也就告一段落了,这边文章是基于我的基础和学习过程所写,算是我对动态代理的一份学习笔记,同时也希望能对在学习动态代理遇到问题的人寻求答案的人帮助有所。如果错误或是不详,请各位大佬帮忙指出。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值