JDK动态代理源码分析

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

JDK动态代理样例

一个接口IHello.java:

1package com.example.demo.proxy.jdk; 2 3public interface IHello { 4 void sayHello(String name); 5}

接口实现类HelloImpl.java:

1package com.example.demo.proxy.jdk; 2 3public class HelloImpl implements IHello { 4 @Override 5 public void sayHello(String name) { 6 System.out.println("Hello " + name); 7 } 8}

一个调用处理器MyInvocationHandler.java:

1package com.example.demo.proxy.jdk; 2 3import java.lang.reflect.InvocationHandler; 4import java.lang.reflect.Method; 5 6public class MyInvocationHandler implements InvocationHandler { 7 8 private Object target; 9 10 public MyInvocationHandler(Object target){ 11 this.target = target; 12 } 13 14 @Override 15 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 16 System.out.println(">>>> Before method invocation"); 17 18 // execute the target method 19 Object result = method.invoke(target, args); 20 System.out.println(">>>> After method invocation"); 21 22 // return the original value 23 return result; 24 } 25}

主测试类JdkProxyDemo.java:

1package com.example.demo.proxy.jdk; 2 3import java.lang.reflect.Proxy; 4 5/** 6 * JDK dynamic proxy demo. 7 * Main implementation steps: 8 * 1. Implement the java.lang.reflect.InvocationHandler interface 9 * 2. Create customized InvocationHandler to receive method calls from interface 10 * 3. Create proxy object by Proxy.newProxyInstance() 11 * 4. Call method on proxy object 12 */ 13public class JdkProxyDemo { 14 public static void main(String[] args) throws Exception { 15 // Save the generated files during executing this program 16 // (class com.misc.ProxyGenerator has logic to use 'saveGeneratedFiles' attribute, 17 // open ProxyGenerator class for detail) 18 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); 19 20 //2. Create customized InvocationHandler to receive method calls from interface 21 MyInvocationHandler handler = new MyInvocationHandler(new HelloImpl()); 22 23 //3. Create proxy object by Proxy.newProxyInstance() 24 IHello hello = (IHello)Proxy.newProxyInstance(IHello.class.getClassLoader(), new Class[]{IHello.class}, handler); 25 26 //4. Call method on proxy object 27 hello.sayHello("Doris"); 28 29 System.out.println(); 30 } 31}

执行结果:

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

源码分析

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

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

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

主要的逻辑就3步:

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

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

这里的proxyClassCache是WeakCache类型:

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

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

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

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

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

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

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

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

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

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

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

在IDEA中对它进行反编译:

可以看到,代理类$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)。

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

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

1//2. Create customized InvocationHandler to receive method calls from interface 2MyInvocationHandler handler = new MyInvocationHandler(new HelloImpl()); 3 4//3. Create proxy object by Proxy.newProxyInstance() 5IHello hello = (IHello)Proxy.newProxyInstance(IHello.class.getClassLoader(), new Class[]{IHello.class}, handler); 6 7//4. Call method on proxy object 8hello.sayHello("Doris");

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

1@Override 2public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 3 System.out.println(">>>> Before method invocation"); 4 5 // execute the target method 6 Object result = method.invoke(target, args); 7 System.out.println(">>>> After method invocation"); 8 9 // return the original value 10 return result; 11}

于是有了上述执行结果。

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

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

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

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

点赞
收藏

评论区

加载中...

相关推荐

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

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )

JDK动态代理源码分析 - HelloWorld