【设计模式】动态代理

其实这篇文章已经写完很久了,但是最近沉迷于JUC源码,所以一直放在草稿箱没有发。这几天把AQS以及相关的子类撸了一遍,写了几篇JUC源码解读,准备发到公号上面来,于是乎决定把草稿箱的这篇文章先解决了

我上一篇文章已经通过代购的例子讨论了静态代理模式,也知道了它不满足开闭原则,扩展性差的缺点,因此,衍生出了动态代理。动态代理分两种,一种是基于接口的,也是jdk本身支持的,还有一种是基于类的,需引入第三方类cglib来实现,今天我们主要讨论基于接口的动态代理(看这篇之前建议看看我前一篇讲静态代理的文章,代购小哥发家史还是值得学习的

一.  概述

其实动态代理的类之间的关系和静态代理是一样

JDK的动态代理类需要依靠两个类(接口)来实现,Proxy静态类,InvocationHandler接口。Proxy类的职责是产生代理Class对象与实例;InvocationHandle接口的职责是负责调用服务和增强服务的(这两个类与接口都严格的遵守了设计模式中的单一职责原则)

是不是有点懵逼,我们还是继续通过代购小哥来理解吧(不了解代购小哥发家史的,可以看看我公号文章:“通过代购理解:静态代理”)

二.  如何使用动态代理

代购小哥生意越做越大,一个人已经忙不过来了,所以他决定召集周围的代购同行,成立一个代购公司。以后来一个顾客,代购公司就分配一个代购小哥哥给他,这样,就可以满足更多妹子的买买买需求啦

这个代购公司的员工就是由Proxy静态类来生成,员工所需要执行的服务则是由实现InvocationHandler接口的类来确定的,我们看下代码吧。

首先我们建立包包商店和鞋鞋商店的接口与实现类

public interface IBagShop {
    void buyBag(String num);
}
public class BagShop implements IBagShop {
    @Override
    public void buyBag(String money) {
        System.out.println("买了一个价值"+money+"限量款包包!");
    }
}
public interface IShoesShop {
    void buyShoes(String num);
}
public class ShoesShop implements IShoesShop{
    @Override
    public void buyShoes(String money) {
        System.out.println("买了一双价值" + money + "的乔丹!!");
    }
}

然后我们再看看代理公司类咋写

public class PurchasingAgentCompany implements InvocationHandler {

    private Object shop;
    
    public void setShop(Object shop) {
        this.shop = shop;
    }

    //通过Proxy获取动态代理的对象(代购小哥)
    public Object getProxyInstance() {
        return Proxy.newProxyInstance(shop.getClass().getClassLoader(), shop.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("代购售前服务:砍价");
        Object ret = method.invoke(shop, args);
        System.out.println("代购售后服务:送小礼品");
        return ret;
    }
}

我们可以看到代购公司PurchasingAgentCompany类继承了InvocationHandler接口,并实现了其invoke方法来调用服务和增强服务

而getProxyInstance方法中使用了Prxoy静态类的newProxyInstance方法来获取代理对象

最后我们创建一个妹子类来找代购公司买东西

public class Girl {
    public static void main(String[] args) {
        // 包包商店 与 鞋鞋商店
        IBagShop bagShop = new BagShop();
        IShoesShop shoesShop = new ShoesShop();
        // 成立一家代购公司
        PurchasingAgentCompany company = new PurchasingAgentCompany();

        // 一个顾客买包(所以代购公司需要引入实际的服务提供者:包包商店)
        company.setShop(bagShop);
        // 代购公司分配了一个包包商店的代购bagShopProxy
        IBagShop bagShopProxy = (IBagShop) company.getProxyInstance();
        // 代购买包包
        bagShopProxy.buyBag("10万");

        System.out.println("------------------------------------");
        // 一个顾客买鞋子(所以代购公司需要引入实际的服务提供者:谢谢商店)
        company.setShop(shoesShop);
        // 代购公司分配了一个鞋鞋商店的代购shoesShopProxy
        IShoesShop shoesShopProxy = (IShoesShop) company.getProxyInstance();
        // 代购买鞋鞋
        shoesShopProxy.buyShoes("1万");

    }
}

我们可以看到,如果我们顾客还需要代购公司代购家具,我们只需要扩展家具接口与其实现类,妹子就可以直接通过代购公司类来购买了,而代购公司的代码不需要动一下。所以动态代理类很好的保证了开闭原则。我们看下输出结果

通过上面的例子与代码,相信大家已经知道如何通过Proxy静态类和InvocationHandler接口来实现动态代理了。

但大家现在一定还是会一脸懵逼,Proxy是如何创造出代理对象的呢?代理对象又是怎么通过实现InvocationHandler的invoke方法来调用服务呢?动态代理用起来是很简单的,但如何实现动态代理才是我们需要去关注的。下面我们通过源码来看看是如何实现动态代理的。

三.  源码解读

前面一节我们知道,方法newProxyInstance是生成动态代理对象的,而生成动态代理对象可以分为两步,首先我们需要生成Class对象,然后再利用反射技术通过Class对象和InvocationHandler对象来生成Class对象的实例对象,即动态代理对象。那我们按照步骤来分析源码吧

我们看下Proxy静态类的newProxyInstance方法

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
    throws IllegalArgumentException
{
    // 省略 ...
    final Class<?>[] intfs = interfaces.clone(); 
    // 省略 ...
    Class<?> cl = getProxyClass0(loader, intfs);

    // 省略 ...
    final Constructor<?> cons = cl.getConstructor(constructorParams);
    // 省略 ...
    return cons.newInstance(new Object[]{h});
}

可以看到,上面就4行源码,为什么只有4行呢?因为与生成代理对象直接相关的源码就这4行啊,其他都是安全检查校验之类的,所以我们抓主干,其他的大家有兴趣可以自己看看(我反正是没太多兴趣,哈哈,至少我现阶段是没必须要去追求这种细枝末节与主干无关的逻辑)我们先解释下这4行代码是干啥的:

  1. 我们首先将传入的接口通过clone方法复制一份(即代购实例中的IShoesShop与IBagShop接口),

  2. 通过传入类加载器与接口(被代理对象的类加载器与其实现的接口)给getProxyClass0方法生成代理Class对象(即代购例子中的ShoesShop和BagShop类的代理Class对象),

  3. 通过Class对象获得构造器

  4. 通过构造器生成Class对象的实例对象(构造器的参数是实现了InvocationHandler接口的对象,即代购例子中的代购公司PurchasingAgentCompany)

所以第1、2行代码是生成我们代理Class对象的,我们先把重点放在弄清楚如何生成代理Class对象,并且知道这个代理Class对象到底是长啥样的,只有清楚这两点,我们才能理解3、4行代码。

既然要知道如何生成代理Class对象,我们就要进入getProxyClass0方法 

private static Class<?> getProxyClass0(ClassLoader loader,
                                       Class<?>... interfaces) {
    if (interfaces.length > 65535) {
        throw new IllegalArgumentException("interface limit exceeded");
    }
    return proxyClassCache.get(loader, interfaces);
}

getProxyClass0方法首先会检查下接口长度,如果没超过65535字节,则会通过调用WeakCache类中的get方法。proxyClassCache即为WeakCache的实例。

我们继续进入proxyClassCache.get方法 

public V get(K key, P parameter) {
    // 省略 ...
    Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
    // 省略 ...
}

subKeyFactory是Proxy静态类中的内部静态类ProxyClassFactory的实例,我们看这个名字就知道(工厂类啊),最终生成的代理Class对象应该就在这个内部静态类ProxyClassFactory中的某个方法中,没错,就是apply方法中。好,我们进入subKeyFactory.apply方法中看看

public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {
    // 省略 ...
    byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces, accessFlags);
    try {
        return defineClass0(loader, proxyName, proxyClassFile, 0, proxyClassFile.length);
    } catch (ClassFormatError e) {
        throw new IllegalArgumentException(e.toString());
    }
    // 省略 ...
}

可以看里面ProxyGenerator.generateProxyClass这个方法吗,只要有代理类名字(代理类的名字是apply方法中生成的,代码被我省略掉了)和被代理类所继承的接口(为啥需要接口,因为接口里面有我们需要的方法呀),我们就可以通过ProxyGenerator.generateProxyClass方法产生二进制数据类型的Class文件ProxyGenerator这个类是sun公司提供的sun.misc包中的一个类,这里我们就不展开了)

再通过defineClass0方法将此class文件通过类加载器生成Class对象放在元空间(1.8之前叫方法区),看到没,Class对象就是这样产生的

我们一般都是先写java文件,然后编译成class文件,最后再生成class对象及其实例但是我们生成的代理Class文件是直接通过generateProxyClass方法在内存中生成的,它没有java文件,所以我们不知道这个代理对象到底长啥样,这里我们可以用反编译将生成的class文件反编译成java文件

我们写了一个小工具类来反编译生成BagShop的代理Class文件的java文件(工具代码和反编译网址贴在最后了) 


import com.yy.demo16_proxyPattern.IBagShop;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class IbagShop extends Proxy implements IBagShop {

   private static Method m1;
   private static Method m3;
   private static Method m2;
   private static Method m0;

   public IbagShop(InvocationHandler var1) throws  {
      super(var1);
   }

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

   public final void buyBag(String var1) throws  {
      try {
         super.h.invoke(this, m3, 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 int hashCode() throws  {
      try {
         return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue();
      } catch (RuntimeException | Error var2) {
         throw var2;
      } catch (Throwable var3) {
         throw new UndeclaredThrowableException(var3);
      }
   }

   static {
      try {
         m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
         m3 = Class.forName("com.yy.demo16_proxyPattern.IBagShop").getMethod("buyBag", new Class[]{Class.forName("java.lang.String")});
         m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
         m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
      } catch (NoSuchMethodException var2) {
         throw new NoSuchMethodError(var2.getMessage());
      } catch (ClassNotFoundException var3) {
         throw new NoClassDefFoundError(var3.getMessage());
      }
   }
}

可以看到,这个类继承了Proxy类,实现了IBagShop接口。我们还可以看到一个有参构造方法,参数是InvocationHandler类型,然后我们再看buyBag这个方法,里面方法的调用语句为:

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

这不就是h通过invoke来调用方法吗(对invoke不了解的可以补一下反射相关的知识)。那h是啥,不就是构造函数传进来的InvocationHander吗?我们再回到最开始的newProxyInstance方法,看下它的第3、4行代码:

final Constructor<?> cons = cl.getConstructor(constructorParams);
return cons.newInstance(new Object[]{h});

上面第一行代码是获取代理Class对象的构造器,第二行代码就是通过构造器生成代理Class对象的实例对象,即我们的动态代理对象。

最后总结一下生成动态代理对象的步骤

  1. sun.misc.generateProxyClass类的generateProxyClass方法根据传入的被代理对象继承的接口信息直接生成代理对象的Class文件

  2. defineClass0方法通过被代理对象类加载器将代理对象的Class文件加载成Class对象

  3. 我们通过反射技术获得Class对象的有参构造器,参数是实现了InvocationHandler接口的代理类

  4. 最后通过构造器生成动态代理对象

最后的最后,我把前文提到的工具代码和反编译网站贴出来


package com.yy.demo16_proxyPattern;

import com.yy.demo1_mapStruct.User;
import sun.misc.ProxyGenerator;
import java.io.FileOutputStream;
import java.io.IOException;

/**
 * @Author: 24只羊
 * @Description:
 * @Date: 2020-01-17
 */
public class ProxyGeneratorUtil {

    private static String DEFAULT_CLASS_NAME = "$Proxy";

    public static byte [] saveGenerateProxyClass(String proxyName, Class clazz) {

        byte[] classFile = ProxyGenerator.generateProxyClass(proxyName, new Class[]{clazz});
        FileOutputStream out = null;

        try {
            String filePath = "这里填写自己" + proxyName + ".class";
            out = new FileOutputStream(filePath);
            out.write(classFile);
            out.flush();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                out.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return classFile;
    }

    public static void main(String[] args) {
        Class<?> interfaces [] = {User.class};
        saveGenerateProxyClass("程序员进阶之路")
    }
}

在线反编译链接地址:http://javare.cn/

(完)

欢迎大家关注我的公众号 “程序员进阶之路”,里面记录了一个非科班程序员的成长之路

                                                        

发布了114 篇原创文章 · 获赞 199 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/qq_36582604/article/details/105108232