JDK动态代理源码分析

Wesley13
• 阅读 789

JDK动态代理,只能代理接口,为什么呢?我们从一个样例入手。

JDK动态代理样例

一个接口IHello.java:

package com.example.demo.proxy.jdk;

public interface IHello {
    void sayHello(String name);
}

接口实现类HelloImpl.java:

package com.example.demo.proxy.jdk;

public class HelloImpl implements IHello {
    @Override
    public void sayHello(String name) {
        System.out.println("Hello " + name);
    }
}

一个调用处理器MyInvocationHandler.java:

package com.example.demo.proxy.jdk;

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

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(">>>> Before method invocation");

        // execute the target method
        Object result = method.invoke(target, args);
        System.out.println(">>>> After method invocation");

        // return the original value
        return result;
    }
}

主测试类JdkProxyDemo.java:

package com.example.demo.proxy.jdk;

import java.lang.reflect.Proxy;

/**
 * JDK dynamic proxy demo.
 * Main implementation steps:
 * 1. Implement the java.lang.reflect.InvocationHandler interface
 * 2. Create customized InvocationHandler to receive method calls from interface
 * 3. Create proxy object by Proxy.newProxyInstance()
 * 4. Call method on proxy object
 */
public class JdkProxyDemo {
    public static void main(String[] args) throws Exception {
        // Save the generated files during executing this program
        // (class com.misc.ProxyGenerator has logic to use 'saveGeneratedFiles' attribute,
        // open ProxyGenerator class for detail)
        System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

        //2. Create customized InvocationHandler to receive method calls from interface
        MyInvocationHandler handler = new MyInvocationHandler(new HelloImpl());

        //3. Create proxy object by Proxy.newProxyInstance()
        IHello hello = (IHello)Proxy.newProxyInstance(IHello.class.getClassLoader(), new Class[]{IHello.class}, handler);

        //4. Call method on proxy object
        hello.sayHello("Doris");

        System.out.println();
    }
}

执行结果:

JDK动态代理源码分析

可以看到,原始的接口实现类HelloImpl的sayHello()方法已经被代理了,在原始逻辑前后分别加入了新的逻辑。

源码分析

接下来从源码的角度分析,其中最重要的是这一句:

IHello hello = (IHello)Proxy.newProxyInstance(IHello.class.getClassLoader(), new Class[]{IHello.class}, handler);

这里调用了java.lang.reflection.Proxy类的方法初始化了一个代理对象:

JDK动态代理源码分析

主要的逻辑就3步:

第1步.  Class<?> cl = getProxyClass0(loader, intfs); 

这一步是根据参数classLoader和指定的interfaces,从缓存中获取代理对象的Class对象,如果缓存中没有,就通过ProxyClassFactory创建:

JDK动态代理源码分析

这里的proxyClassCache是WeakCache类型:

JDK动态代理源码分析

JDK动态代理源码分析

这里的Factory是WeakCache类中的私有内部类:

JDK动态代理源码分析

在Proxy类中创建代理类缓存WeakCache的参数:

JDK动态代理源码分析

来看下ProxyClassFactory中创建代理类过程(apply()方法):

JDK动态代理源码分析

生成代理类的方法ProxyGenerator.generateProxyClass():

JDK动态代理源码分析

注意:这里的参数saveGeneratedFiles是获取的系统属性:

private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(
        new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

在GetBooleanAction类中有个run方法,会通过设置给GetBooleanAction的属性(即这里的sun.misc.ProxyGenerator.saveGeneratedFiles)返回一个Boolean值,跟进去可以看到,最终是通过解析System.getProperties()的值返回的:

JDK动态代理源码分析

这就是一开始为了保存生成的代理类文件,通过代码设置这个属性的原因:

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

第2步. final Constructor<?> cons = cl.getConstructor(constructorParams);

执行完第1步后,代理类的字节码已经由ProxyClassFactory类生成了,而且已经保存下来,我们打开项目的根目录,找到这个文件:

JDK动态代理源码分析

在IDEA中对它进行反编译:

JDK动态代理源码分析

可以看到,代理类$Proxy0有以下特点:

  • 继承自JDK自带的Proxy类,有一个参数为InvocationHandler的构造函数(这正是这一步需要通过反射来获得的,用于生成代理对象)
  • 覆盖了Object类的toString(), equals()和hashCode()方法,
  • 实现了IHello的sayHello()方法

重点关注实现的sayHello()方法:

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

里面调用了父类Proxy的InvocationHandler对象(属性h)的invoke方法,第一个参数this代表是代理对象本身,第二个参数是IHello.sayHello()方法,第三个参数是sayHello()的参数。

这就是说,代理对象会将sayHello()方法的调用,转发给InvocationHandler.invoke()方法

第3步. return cons.newInstance(new Object[]{h});

用第2步取得的构造器初始化一个代理对象,并将参数InvocationHandler传递给代理对象用于初始化。
上面可以看到,在$Proxy0的构造函数中,将参数传递给了父类Proxy进行初始化。
因此在调用代理对象的sayHello()方法时,实际上此调用会转发给这个InvocationHandler(即前文的MyInvocationHandler)。

=========================分隔线=====================================

了解了动态代理的原理后,现在来重新分析开始的代码:

//2. Create customized InvocationHandler to receive method calls from interface
MyInvocationHandler handler = new MyInvocationHandler(new HelloImpl());

//3. Create proxy object by Proxy.newProxyInstance()
IHello hello = (IHello)Proxy.newProxyInstance(IHello.class.getClassLoader(), new Class[]{IHello.class}, handler);

//4. Call method on proxy object
hello.sayHello("Doris");

2. 我们先创建了一个自定义的代理类MyInvocationHandler(继承自InvocationHandler),然后创建了一个对象handler,handler里面的target是一个HelloImpl对象。
3. 接下来用这个handler创建了一个IHello的代理对象hello。
4. 调用代理对象的sayHello("Doris")方法时,此调用转发给代理对象hello的InvocationHandler(即这里的handler对象)。
在handler执行的时候,调用到了这段代码:

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.println(">>>> Before method invocation");

    // execute the target method
    Object result = method.invoke(target, args);
    System.out.println(">>>> After method invocation");

    // return the original value
    return result;
}

于是有了上述执行结果。

=========================分隔线=====================================

回答开始的问题:为什么JDK动态代理只能代理接口?
因为生成的代理类默认继承了JDK的Proxy类,由于Java是单继承,所以此代理类只能通过实现接口的方式扩展更多的方法,所以只能代理接口了

=========================分隔线=====================================

源码:https://github.com/wanxiaolong/JavaProxyDemo

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
3年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这