面试官:为什么jdk动态代理只能代理接口实现类?
**本文首发于公众号【看点代码再上班】,欢迎围观,第一时间获取最新文章。**
原文地址:http://mp.weixin.qq.com/s/LWToCjGGP52_0cy9xkiHlQ
大家好,我是tin,这是我的第15篇原创文章
我们日常业务能够使用jdk动态代理编码的场景非常少,但是我们使用的框架用到jdk动态代理的却非常多,今天结合jdk动态代理源码讲一讲“为什么jdk动态代理只能代理接口?”,先上一个目录:
-
二、复现问题“jdk动态代理不能代理普通类”
-
三、深入源码分析
-
四 、结语
一、把jdk动态代理跑起来
顾名思义,jdk动态代理是jdk实现的一个功能,不需要第三方库支持,我们配置好依赖jdk到我们的工程即可使用,要把jdk动态代理跑起来,非常简单:
-
业务目标对象实现接口;
-
实现InvocationHandler接口;
-
使用Proxy.newProxyInstance生成代理对象;
定义业务BookFacade接口:
BookFacadeImpl接口实现:
定义MyInvocationHandler实现InvocationHandler接口:
package com.tin.example.jdk.proxy; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * title: MyInvocationHandler * <p> * description: * * @author tin @公众号【看点代码再上班】 on 2022/1/22 上午9:01 */ public class MyInvocationHandler implements InvocationHandler { //目标对象 private Object target; public Object getInstance(Object target) { this.target = target; Class clazz = this.target.getClass(); // Proxy.newProxyInstance的三个参数分别是: // 1 被代理类的类加载器 // 2 被代理类的接口 // 3 java.lang.reflect.InvocationHandler return Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), this); } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { System.out.println("------前置通知------------"); // 执行目标方法 Object result = method.invoke(target, args); System.out.println("------后置处理------------"); return result; } }
MyInvocationHandler类内的Proxy.newProxyInstance生成代理对象。通过以上类定义,我们来写一个main测试类:
package com.tin.example.jdk.proxy; /** * title: JdkProxyTest * <p> * description: * * @author tin @公众号【看点代码再上班】 on 2022/1/22 上午9:11 */ public class JdkProxyTest { public static void main(String[] args) throws Exception { //sun.misc.ProxyGenerator.saveGeneratedFiles 用于输出代理类class文件到本地 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //代理接口 BookFacade proxy = (BookFacade) new MyInvocationHandler().getInstance(new BookFacadeImpl()); proxy.addBook("Java性能权威指南@【看点代码再上班】"); } }
main方法运行结果如下:
因为我的测试类已经加了以下代码:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
这是用于把生成的proxy代理类的class文件输出到本地的,打开生成的class文件,内容如下:
同样的,生成的代理类实现了目标接口定义的方法addBook():
到这里就可以初步回答我们标题的问题了:
jdk动态代理生成的代理类实现了我们业务定义的接口,并重写了我们接口的方法,如此才实现代理的功能,所以我们的目标类需要实现接口。
二、复现问题“jdk动态代理不能代理普通类”
看了以上示例估计就有朋友反问了,所举例子只是一个特例,说明接口实现类可以被jdk代理,非接口实现类也可以吧,不一定是通过"implements"方式实现,这就是我们这小节的问题“jdk动态代理能不能代理普通类?”。
新建一个没有实现接口的类BookOperation:
public class BookOperation { public void addBook(String bookName) { System.out.println("添加书籍, bookName:" + bookName); } }
Test测试类如下:
public class JdkProxyTest { public static void main(String[] args) throws Exception { //sun.misc.ProxyGenerator.saveGeneratedFiles 用于输出代理类class文件到本地 System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); //代理普通类 BookOperation bookOperation = (BookOperation) new MyInvocationHandler().getInstance(new BookOperation()); bookOperation.addBook("Java性能权威指南@【看点代码再上班】"); } }
依然通过类强转的方式获取生成的类:
BookOperation bookOperation = (BookOperation) new MyInvocationHandler().getInstance(new BookOperation());
有朋友可能会反问,它不一定是生成BookOperation类,这样取是不是不合适的?是的,这样的提问是没问题的,但是如果不强转,我不知道如何能够调用我们的目标方法。
为了能够验证需要类型强转且确确实实无法代理非接口实现类,我们看下面的运行结果以及生成的代理类的结构。
运行结果如下:
从抛的异常来看,代理类Proxy已经生成,但不是我想要的BookOperation的代理类!类型强转失败。
既然不是我指定的目标类的代理类,那么生成的Proxy类究竟是怎么样的呢?
见下图:
和我们前面的Proxy最大的不同在于没有实现接口(因为BookOperation没有实现接口,这是必然的), 整个Proxy内也没有addBook()方法。
既然没有目标类的方法,代理类如何执行我们的目标方法?
“ 既然没有接口,那为什么不能继承BookOperation类完成代理功能?! ”会有人这么想。
这样想就对了,这个也就是cglib采用的方式,具体原理请阅读以下文章啦:
到这里,我们可以再次确定标题所问之答案:jdk动态代理无法代理非接口实现类。
三、深入源码分析
以上只是通过表层分析我们的jdk动态代理无法代理非接口实现类。是否真如此呢?我们通过源码看一看。
jdk代理的关键在代理类的生成。我们从Proxy.newProxyInstance()方法切入,深入分析代理类是如何生成的。
@CallerSensitive public static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h) throws IllegalArgumentException { Objects.requireNonNull(h); final Class<?>[] intfs = interfaces.clone(); final SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkProxyAccess(Reflection.getCallerClass(), loader, intfs); } /* * Look up or generate the designated proxy class. * ① 寻找或者生成(如果缓存没有)指定代理类 */ Class<?> cl = getProxyClass0(loader, intfs); /* * Invoke its constructor with the designated invocation handler. */ try { if (sm != null) { checkNewProxyPermission(Reflection.getCallerClass(), cl); } /* * ② 获取构造器 */ final Constructor<?> cons = cl.getConstructor(constructorParams); final InvocationHandler ih = h; if (!Modifier.isPublic(cl.getModifiers())) { AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { cons.setAccessible(true); return null; } }); } /* * ③ 根据我们定义的invocation handler生成代理对象 */ return cons.newInstance(new Object[]{h}); } catch (IllegalAccessException|InstantiationException e) { throw new InternalError(e.toString(), e); } catch (InvocationTargetException e) { Throwable t = e.getCause(); if (t instanceof RuntimeException) { throw (RuntimeException) t; } else { throw new InternalError(t.toString(), t); } } catch (NoSuchMethodException e) { throw new InternalError(e.toString(), e); } }
Proxy.newProxyInstance()关键在两步:
-
① Class<?> cl = getProxyClass0(loader, intfs) 生成代理类;
-
② return cons.newInstance(new Object[]{h}) 生成代理对象;
第①点,生成代理类,就是生成我们以上com/sun/proxy/$Proxy0.class代理类的字节码,继续跟进去看一下。
getProxyClass0方法代码很少,其内部直接调用了proxyClassCache.get方法:
proxyClassCache是一个本地静态变量也是一个缓存cache,其构造器的两个入参KeyFactory和ProxyClassFactory至关重要,分别对应 keyFactory和valueFactory :
继续进入到proxyClassCache.get()方法内部,如下:
看上图的第③点,获取最终的代理类value值重点就在于Factory的get()方法:
前面讲proxyClassCache的构造器入参时已经提到valueFactory对应的就是ProxyClassFactory。valueFactory.apply其实对应的是ProxyClassFactory#apply。
我们继续跟进去看看ProxyClassFactory#apply方法逻辑(ProxyClassFactory也是Proxy的一个内部类):
第①点,
interfaceClass = Class.forName(intf.getName(), false, loader);
验证入参类加载器按全限定名称重新加载入参接口得到的class对象是否和接口class对象相同,主要也是为了验证类加载器是否相同,因为同一个类加载器加载的class才相同,否则不相同:
第①点,
if (!interfaceClass.isInterface())
验证入参class对象是否是接口。
其实,如果目标类没有实现接口,interfaces数组会为空,不会走到这里,我们启动非接口实现类debug如下:
interfaces为空,会导致最终生成的代理类缺少相关的方法。继续跟进到第③点 生成代理类 一看究竟。
sun.misc.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int)方法如下:
最终生成class是在generateClassFile(), 该方法生成类的方法和属性等信息,它会遍历interfaces中的每一个接口class,把class的每一个方法包装为ProxyMethod对象,最后通过ProxyMethod生成代理类的代理方法并输出到字节流中,如下图:
从以上图示可以很明显的看出来,非接口实现类传的参数interfaces是一个空数组,最后generateClassFile()无法生成对应的代理方法。
以上,从源码维度也佐证了jdk动态代理依赖接口,只有实现了接口的类的方法才能被加入到代理类中生成代理方法,最终完成代理功能。
四、结语
我是tin,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎加我提出,我一定细心推敲并加以修改。
坚持创作不容易,你的正反馈是我坚持输出的最强大动力,谢谢!
最后别忘了关注我公众号哦【看点代码再上班】!附上原文链接:arrow_double_down::arrow_double_down::arrow_double_down:
http://mp.weixin.qq.com/s/LWToCjGGP52_0cy9xkiHlQ
- 面试官:cglib为什么不能代理private方法?
- 想看Dubbo源码?一定要先看一看这一篇!
- 死磕synchronized二:系统剖析延迟偏向篇一
- 架构与思维:高并发下解决主从延时的一些思路
- 道与术
- OopMap看不懂,怎么调优哇
- Kafka 精妙的高性能设计(下篇)
- 一次tcp窗口被填满问题的排查实践
- 抢了个票,还以为发现了12306的系统BUG
- 微服务5:服务注册与发现(实践篇)
- 看一遍就理解:零拷贝详解
- 分布式:分布式系统下的唯一序列
- 面试官:为什么jdk动态代理只能代理接口实现类?
- 揭开内存屏障的神秘面纱
- 微服务4:服务注册与发现
- 我就奇了怪了,STW到底是怎么做到的
- 这样使用 IDEA ,效率提升10倍!| IDEA 高效使用指南
- 从hotspot源码层面剖析Java的多态实现原理
- 垃圾回收全集之十二:GC 调优的实战篇—Weak, Soft 及 Phantom 引用
- JVM的多态是如何实现的