【小家Java】JDK动态代理技术,你真学会了吗?(Proxy、ProxyClassFactory)

简介: 【小家Java】JDK动态代理技术,你真学会了吗?(Proxy、ProxyClassFactory)

前言


java界有个熟语:反射是你通向高级的敲门砖,而动态代理是你站稳高级的基础。


动态代理技术,相信我们都并不陌生。特别是在Spring框架内,大量的使用到了反射以及动态代理技术。但是如果我们只是停留在平时的运用阶段,此篇文章你其实是可以跳过的,因为反射、代理技术一般都只有在框架设计中才会使用到,业务开发是不用接触的。


一般而言,动态代理分为两种,一种是JDK反射机制提供的代理,另一种是CGLIB代理。本文主要介绍JDK动态代理的基本原理,让大家更深刻的理解JDK Proxy,知其然知其所以然。(CGLIB代理,这里也会给个Demo知道就行)


JDK动态代理真正的原理及其生成的过程


准备工作


先准备一个接口,一个实现类:

public interface Helloworld {
    void sayHello();
}
public class HelloworldImpl implements Helloworld {
    @Override
    public void sayHello() {
        System.out.print("hello world");
    }
}


JDK动态代理Demo


准备一个必须的InvocationHandler


public class MyInvocationHandler implements InvocationHandler {
    private Object target;
    public MyInvocationHandler(Object target) {
        this.target = target;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("method :" + method.getName() + " is invoked!");
        return method.invoke(target, args);
    }
}


然后写个Main方法实践一下:


方式一:


    public static void main(String[] args) throws Exception {
      // 传入三大参数,就能够创建出一个代理对象
    Helloworld helloWorld = (Helloworld) Proxy.newProxyInstance(
          ProxyTest.class.getClassLoader(),
          new Class<?>[]{Helloworld.class},
          new MyInvocationHandler(new HelloworldImpl())); //此处目标实现为HelloworldImpl
        helloWorld.sayHello();
    }
输出:
method :sayHello is invoked!
hello world


此种方式也是最简便的,也是最被我们熟知的创建代理的方式(所谓三大参数)


方式二:


接下来介绍另外一种方式,平时我们使用较少,了解一下即可


    public static void main(String[] args) throws Exception {
        // 三个步骤:
        // 1、生成代理接口的Class() class com.sun.proxy.$Proxy0
        // 2、拿到构造器:public com.sun.proxy.$Proxy0(java.lang.reflect.InvocationHandler)
        // 3、new一个InvocationHandler实例~~~
        Class<?> proxyClass = Proxy.getProxyClass(ProxyTest.class.getClassLoader(), Helloworld.class);
        Constructor<?> cons = proxyClass.getConstructor(InvocationHandler.class);
        InvocationHandler ih = new MyInvocationHandler(new HelloworldImpl());
        // 通过构造函数 new出一个实例
        Helloworld helloWorld = (Helloworld) cons.newInstance(ih);
        helloWorld.sayHello();
    }
输出:
method :sayHello is invoked!
hello world


方式一的基本原理是方式二


CGLIB代理Demo


关于CGLIB的动态代理不是本文的重点,因此此处只给一个Demo:

先写一个增强器(CGLIB内部使用的增强器哦~~~~):


// 注意:这个是org.springframework.cglib.proxy.MethodInterceptor
// 而不是org.aopalliance.intercept包下的
public class MyMethodInterceptor implements MethodInterceptor {
    @Override
    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
        Object intercept = methodProxy.invokeSuper(object, args); // 注意这里调用的是methodProxy.invokeSuper
        System.out.println("中介:该房源已发布!");
        return intercept;
    }
}


main方法运行:


  // 此处需要说明:Enhancer实际属于CGLIB包的,也就是`net.sf.cglib.proxy.Enhancer`
  // 但是Spring把这些类都拷贝到自己这来了,因此我用的Spring的Enhancer,包名为;`org.springframework.cglib.proxy.Enhancer`
    public static void main(String[] args) throws Exception {
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(HelloServiceImpl.class); // 注意此处的类型必须是实体类
        enhancer.setCallback(new MyMethodInterceptor());
        HelloServiceImpl helloService = (HelloServiceImpl) enhancer.create();
        helloService.hello();
    }
输出;
this is my method~~
中介:该房源已发布!


JDK代理生成过程(原理)


我们之所以天天叫JDK动态代理,是因为这个代理class是由JDK在运行时动态帮我们生成。

为了更好的讲述,请在JVM的启动参数上加上如下启动参数:


-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true


这个参数的作用:帮我们把JDK动态生成的proxy class 的字节码保存到硬盘中,然后我们方便查看了


我的用的idea,运行main方法后会生成这个文件如下图:

image.png


可议看到生成的代理类的所在包为:com.sun.proxy,它是默认包名


贴出它生成出来的源码如下:

// 1、所有JDK动态代理  都是Proxy的子类  且自己是final类
// 2、实现了你所需要代理得接口
// 3、代理类整体看起来都是非常简单的  我们发现不管调用哪个方法,最终都是交给了InvocationHandler.invoke()方法  这也就是为什么需要我们提供这个接口的实现类的原因吧
public final class $Proxy0 extends Proxy implements Helloworld {
    private static Method m1;
    private static Method m3;
    private static Method m2;
    private static Method m0;
  // 通过反射给Method赋值   这里我们得出结论
  // Object的三个方法equals/toString/hashCode最终都是会被代理的
  // m3是我们HelloService自己的业务方法
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m3 = Class.forName("com.proxy.Helloworld").getMethod("sayHello");
            m2 = Class.forName("java.lang.Object").getMethod("toString");
            m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
  // 构造函数  
    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 void sayHello() throws  {
        try {
            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 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);
        }
    }
}


从代理出来的类我们可以总结如下(逻辑很简单):


  1. 静态字段:被代理的接口所有方法都有一个对应的静态方法变量;
  2. 静态块:主要是通过反射初始化静态方法变量;
  3. 具体每个代理方法:逻辑都差不多就是 h.invoke,主要是调用我们定义好的invocatinoHandler逻辑,触发目标对象target上对应的方法;
  4. 构造函数:从这里传入我们InvocationHandler逻辑;


Proxy类


一切源于Proxy这个类。我们可以看看

public class Proxy implements java.io.Serializable {
  ,,,
    /**
     * a cache of proxy classes
     */
     //KeyFactory和ProxyClassFactory都是内部类
    private static final WeakCache<ClassLoader, Class<?>[], Class<?>>
        proxyClassCache = new WeakCache<>(new KeyFactory(), new ProxyClassFactory());
  // 这是我们自己实现的执行器~~~
    protected InvocationHandler h;
  // 显然,我们并不能直接new它的对象
  // 它里面所有的方法都是静态方法
    private Proxy() {
    }
    // 这个构造函数由子类调用
    protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
  // 判断是否为Proxy的子类(也就是否为JDK动态代理生成的类)
  // 可以对比一下ClassUtils.isCglibProxyClass(object.getClass())  判断是否是CGLIB对象(其实就是看名字里是否含有 "$$"这个)
    public static boolean isProxyClass(Class<?> cl) {
        return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
    }
  // 这个不用说了,最重要的一个方法:生成代理对象
  public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h) {
    ...
  }
  ...下面的方法再单独分析
}


我们从上面的方式二分析:getProxyClass方法是入口:需要传入类加载器和interface


    @CallerSensitive
    public static Class<?> getProxyClass(ClassLoader loader, Class<?>... interfaces) throws IllegalArgumentException {
    // 克隆一份
        final Class<?>[] intfs = interfaces.clone();
        final SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkProxyAccess(Reflection.getCallerClass(), loader, intfs);
        }
    // 真正做事的是这里
        return getProxyClass0(loader, intfs);
    }


然后调用getProxyClass0方法,方法的JavaDoc其实说得比较清楚了:如果实现当前接口的代理类存在,直接从缓存中返回,如果不存在,则通过ProxyClassFactory来创建


    private static Class<?> getProxyClass0(ClassLoader loader, Class<?>... interfaces) {
      // 这里注意:我们发现允许实现接口的上线是65535个(哇,这也太多了吧  哈哈)
        if (interfaces.length > 65535) {
            throw new IllegalArgumentException("interface limit exceeded");
        }
    // 这个JavaDoc说得很清楚,先从缓存拿,否则用`ProxyClassFactory`创建
        // If the proxy class defined by the given loader implementing
        // the given interfaces exists, this will simply return the cached copy;
        // otherwise, it will create the proxy class via the ProxyClassFactory
        return proxyClassCache.get(loader, interfaces);
    }


ProxyClassFactory


ProxyClassFactory是Proxy的一个静态内部类。它的逻辑包括了下面三步:


  1. 包名的创建逻辑

包名生成逻辑默认是com.sun.proxy,如果被代理类是non-public proxy interface(也就说实现的接口若不是public的,报名处理方式不太一样),则用和被代理类接口一样的包名,类名默认是$Proxy 加上一个自增的整数值


2.调用ProxyGenerator. generateProxyClass生成代理类字节码

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true 这个参数就是在该方法起到作用,如果为true则保存字节码到磁盘。代理类中,所有的代理方法逻辑都一样都是调用invocationHander的invoke方法(上面源码我们也能看出来)


3.把代理类字节码加载到JVM。

把字节码通过传入的类加载器加载到JVM中:defineClass0(loader, proxyName,proxyClassFile, 0, proxyClassFile.length);


总结


通过这一分析,发现JDK的动态代理也不过如此嘛。


由下面几点是需要注意的:


  • toString() hashCode() equal()方法 调用逻辑:这个三个Object上的方法,如果被调用将和其他接口方法方法处理逻辑一样,都会经过invocationHandler逻辑,从上面的字节码结果就可以明显看出。其他Object上的方法将不会走代理处理逻辑,直接走Proxy继承的Object上方法逻辑。
  • interface 含有equals,toString hashCode方法时,和处理普通接口方法一样,都会走invocation handler逻辑,以目标对象重写的逻辑为准去触发方法逻辑
  • interface含有重复的方法签名,以接口传入顺序为准,谁在前面就用谁的方法,代理类中只会保留一个,不会有重复的方法签名;

相关文章
|
1天前
|
Java 关系型数据库 MySQL
"解锁Java Web传奇之旅:从JDK1.8到Tomcat,再到MariaDB,一场跨越数据库的冒险安装盛宴,挑战你的技术极限!"
【8月更文挑战第19天】在Linux上搭建Java Web应用环境,需安装JDK 1.8、Tomcat及MariaDB。本指南详述了使用apt-get安装OpenJDK 1.8的方法,并验证其版本。接着下载与解压Tomcat至`/usr/local/`目录,并启动服务。最后,通过apt-get安装MariaDB,设置基本安全配置。完成这些步骤后,即可验证各组件的状态,为部署Java Web应用打下基础。
7 1
|
5天前
|
Java API Apache
JDK8到JDK24版本升级的新特性问题之在Java中,HttpURLConnection有什么局限性,如何解决
JDK8到JDK24版本升级的新特性问题之在Java中,HttpURLConnection有什么局限性,如何解决
|
5天前
|
Oracle 安全 Java
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
JDK8到JDK28版本升级的新特性问题之在Java 15及以后的版本中,密封类和密封接口是怎么工作的
|
5天前
|
Java
Java BasePooledObjectFactory 对象池化技术
Java BasePooledObjectFactory 对象池化技术
9 1
|
5天前
|
Java API 开发者
JDK8到JDK17版本升级的新特性问题之SpringBoot选择JDK17作为最小支持的Java lts版本意味着什么
JDK8到JDK17版本升级的新特性问题之SpringBoot选择JDK17作为最小支持的Java lts版本意味着什么
JDK8到JDK17版本升级的新特性问题之SpringBoot选择JDK17作为最小支持的Java lts版本意味着什么
|
15天前
|
Java 编译器 测试技术
Java零基础教学(03):如何正确区别JDK、JRE和JVM??
【8月更文挑战第3天】Java零基础教学篇,手把手实践教学!
39 2
|
15天前
|
IDE Java 测试技术
Java零基础(4) - JDK、IntelliJ IDEA的安装和环境变量配置
【8月更文挑战第4天】🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
43 0
Java零基础(4) - JDK、IntelliJ IDEA的安装和环境变量配置
|
5天前
|
Java 编译器 开发者
JDK8到JDK23版本升级的新特性问题之编写一个简单的module-info.java文件,如何实现
JDK8到JDK23版本升级的新特性问题之编写一个简单的module-info.java文件,如何实现
|
5天前
|
Oracle Java 关系型数据库
简单记录在Linux上安装JDK环境的步骤,以及解决运行Java程序时出现Error Could not find or load main class XXX问题
本文记录了在Linux系统上安装JDK环境的步骤,并提供了解决运行Java程序时出现的"Error Could not find or load main class XXX"问题的方案,主要是通过重新配置和刷新JDK环境变量来解决。
17 0
|
5天前
|
存储 设计模式 安全
Java GenericObjectPool 对象池化技术--SpringBoot sftp 连接池工具类
Java GenericObjectPool 对象池化技术--SpringBoot sftp 连接池工具类
5 0