面试官:为什么jdk动态代理只能代理接口实现类?

简介: 以上,从源码维度也佐证了jdk动态代理依赖接口,只有实现了接口的类的方法才能被加入到代理类中生成代理方法,最终完成代理功能。
  本文首发于公众号【看点代码再上班】,建议关注公众号,及时阅读最新文章。
一定要读的原文呀: https://mp.weixin.qq.com/s/LWToCjGGP52_0cy9xkiHlQ

大家好,我是tin,这是我的第15篇原创文章

我们日常业务能够使用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采用的方式,具体原理请阅读以下文章啦:

面试官:cglib为什么不能代理private方法?

到这里,我们可以再次确定标题所问之答案: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,一个在努力让自己变得更优秀的普通工程师。自己阅历有限、学识浅薄,如有发现文章不妥之处,非常欢迎公众号加我提出,我一定细心推敲并加以修改。

坚持创作不容易,你的正反馈是我坚持输出的最强大动力,谢谢!

最后别忘了关注我公众号哦【看点代码再上班】!附上原文链接⏬⏬⏬
https://mp.weixin.qq.com/s/LWToCjGGP52_0cy9xkiHlQ

相关文章
|
3月前
|
Java Linux
java基础(3)安装好JDK后使用javac.exe编译java文件、java.exe运行编译好的类
本文介绍了如何在安装JDK后使用`javac.exe`编译Java文件,以及使用`java.exe`运行编译好的类文件。涵盖了JDK的安装、环境变量配置、编写Java程序、使用命令行编译和运行程序的步骤,并提供了解决中文乱码的方法。
76 2
|
29天前
|
安全 Java 开发者
AOP中的JDK动态代理与CGLIB动态代理:深度解析与实战模拟
【11月更文挑战第21天】面向切面编程(AOP,Aspect-Oriented Programming)是一种编程范式,它通过将横切关注点(cross-cutting concerns)与业务逻辑分离,以提高代码的可维护性和可重用性。在Java开发中,AOP的实现离不开动态代理技术,其中JDK动态代理和CGLIB动态代理是两种常用的方式。本文将从背景、历史、功能点、业务场景、底层逻辑等多个维度,深度解析这两种代理方式的区别,并通过Java示例进行模拟和比较。
44 4
|
2月前
|
设计模式 Java API
[Java]静态代理与动态代理(基于JDK1.8)
本文介绍了代理模式及其分类,包括静态代理和动态代理。静态代理分为面向接口和面向继承两种形式,分别通过手动创建代理类实现;动态代理则利用反射技术,在运行时动态创建代理对象,分为JDK动态代理和Cglib动态代理。文中通过具体代码示例详细讲解了各种代理模式的实现方式和应用场景。
32 0
[Java]静态代理与动态代理(基于JDK1.8)
|
2月前
|
Java
【编程进阶知识】静态代理、JDK动态代理及Cglib动态代理各自存在的缺点及代码示例
本文介绍了三种Java代理模式:静态代理、JDK动态代理和Cglib动态代理。静态代理针对特定接口或对象,需手动编码实现;JDK动态代理通过反射机制实现,适用于所有接口;Cglib动态代理则基于字节码技术,无需接口支持,但需引入外部库。每种方法各有优缺点,选择时应根据具体需求考虑。
23 1
|
2月前
|
设计模式 Java 索引
动态代理总结,面试你要知道的都在这里,无废话!
动态代理总结,面试你要知道的都在这里,无废话!
|
3月前
|
Java API 开发者
【Java字节码操控新篇章】JDK 22类文件API预览:解锁Java底层的无限可能!
【9月更文挑战第6天】JDK 22的类文件API为Java开发者们打开了一扇通往Java底层世界的大门。通过这个API,我们可以更加深入地理解Java程序的工作原理,实现更加灵活和强大的功能。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来!
|
3月前
|
Java API 开发者
【Java字节码的掌控者】JDK 22类文件API:解锁Java深层次的奥秘,赋能开发者无限可能!
【9月更文挑战第8天】JDK 22类文件API的引入,为Java开发者们打开了一扇通往Java字节码操控新世界的大门。通过这个API,我们可以更加深入地理解Java程序的底层行为,实现更加高效、可靠和创新的Java应用。虽然目前它还处于预览版阶段,但我们已经可以预见其在未来Java开发中的重要地位。让我们共同期待Java字节码操控新篇章的到来,并积极探索类文件API带来的无限可能!
|
4月前
|
开发者 C# 容器
【独家揭秘】当WPF邂逅DirectX:看这两个技术如何联手打造令人惊艳的高性能图形渲染体验,从环境搭建到代码实践,一步步教你成为图形编程高手
【8月更文挑战第31天】本文通过代码示例详细介绍了如何在WPF应用中集成DirectX以实现高性能图形渲染。首先创建WPF项目并使用SharpDX作为桥梁,然后在XAML中定义承载DirectX内容的容器。接着,通过C#代码初始化DirectX环境,设置渲染逻辑,并在WPF窗口中绘制图形。此方法适用于从简单2D到复杂3D场景的各种图形处理需求,为WPF开发者提供了高性能图形渲染的技术支持和实践指导。
284 0
|
4月前
|
设计模式 Java C++
揭秘!JDK动态代理VS CGLIB:一场关于Java代理界的‘宫心计’,你站哪队?
【8月更文挑战第24天】Java 动态代理是一种设计模式,允许在不改动原类的基础上通过代理类扩展功能。主要实现方式包括 JDK 动态代理和 CGLIB。前者基于接口,利用反射机制在运行时创建代理类;后者采用继承方式并通过字节码技术生成子类实现类的代理。两者在实现机制、性能及适用场景上有明显差异。JDK 动态代理适用于有接口的场景,而 CGLIB 更适合代理未实现接口的类,尽管性能更优但存在一些限制。开发者可根据需求选择合适的代理方式。
202 0
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!