一文读懂 Java 动态代理,那些面试中你容易忽略的细节!
共 17462字,需浏览 35分钟
·
2021-03-29 11:52
写前言
本来是打算把java代理模式给写一下的,但是整理思路的时候发现这是一个庞大的工程,我需要讲清楚什么是代理模式;它的应用场景有哪些;代理又分为静态代理和动态代理,它们分别是如何实现的,区别又是什么,我还要举例,分析源码,emm。显然,我现在的时间安排是无法完成这个庞大的工程的,所以我就讲一下目前解决问题中遇到的动态代理吧(默认你大致了解代理模式)
为什么要写这篇文章
我最近在学习Retrofit2源码,而这个框架比较核心的一点就是动态代理,所以在这里把我学习过程中的一些我认为比较关键的地方整理出来,分享给有需要的童鞋。
Retrofit2的动态代理到底体现在哪里?请看下面代码
//retrofit的API接口对象
ApiService apiService;
//创建代理对象
apiService = retrofit.create(ApiService.class);
//调用代理类中的方法
apiService.xxx();
这是retrofit.create
方法的源码,很明显的动态代理
动态代理的本质是什么?
我的理解:
提供一个代理来控制对象的访问; 程序运行时动态生成代理类,这个代理类继承于Proxy,并且实现自定义的委托类的接口; 丰富原始类的操作
动态代理的具体实现
本来是不想写一堆代码来说动态代理的代码是如何实现的,但是没办法,有些问题不通过举例无法说清楚,下面一起来看一个简单的例子吧:
学生每学期都需要参加期末考试的
public interface Student {
//参加考试
void exam();
}
学生分为很多专业的,其中计算机专业的学生需要参加计算机专业考试
public class ComputerStudent implements Student {
private String name;
public ComputerStudent(String name) {
this.name = name;
}
@Override
public void exam() {
System.out.println(name + " 参加计算机专业考试");
}
}
由于每个学生的专业能力不一致,有的需要在考试前刷刷题,有的需要去找学霸辅导一下(抱大腿),等等,总之每个学生在考试前都需要做一些事情。
如果现在需要定义很多不同特点的学生,你怎么做呢?如果去一个个定义不同的行为的话,那将是非常庞大的工作量,那有没有简单的办法呢?答案是有的,通过代理类就可以实现。
事实上我们没有必要去定义每个学生,因为每个学生的行为是没办法确定的,我们可以通过动态代理在它做这个动作的时候去实现他的特定行为。
public class Test {
public static void main(String[] args){
//生成$Proxy0的class文件
//System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//被代理类
final Student jack =new ComputerStudent("jack");
//生成代理对象
Student jackProxy= (Student) Proxy.newProxyInstance(jack.getClass().getClassLoader(),
jack.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before exam do something");
//通过反射调用对象的方法
method.invoke(jack,args);
System.out.println("after exam do something");
return null;
}
});
//代理方法调用
jackProxy.exam();
}
}
当调用代理类的exam()
方法,程序运行结果如下
这里有个重点,通过代理类对象jackProxy去调用方法和接口实现类对象jack去调用方法是有明显区别的,通过代理方式去调用,可以在原来方法执行前后做一些其它操作,这就是代理模式的特点
那些你容易忽略的细节
首先回顾一下动态代理的实现流程:
1、通过 Proxy.newProxyInstance
方法创建一个代理对象;2、在内部类 InvocationHandler
的invoke()
方法中做一些操作:利用反射调用被代理类(这里是ComputerStudent)中的方法,通常在这个调用方法前后还会做一些其它操作;3、代理对象jackProxy调用方法 对整个流程有个了解之后,下面来看一些细节问题。
Q1:Proxy.newProxyInstance中的3个参数到底是什么?为什么要穿入这3个参数?
系统接口定义如下:
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
loader
:定义代理类的类加载器,这里要代理的是jack,所以用jack的类加载器interfaces
:是一个接口类的集合,具体来说是代理类实现的接口的集合,也是被代理类实现的接口的集合;h
:代理类对象调用方法时需要用到的一个接口对象,在系统生成的代理类内部会用到它。
到这里,我想细心的童鞋会想这个代理类到底是什么?似乎从头到尾没有露面过。的确是这样,即使你去翻遍源码你也找不到这个代理类,因为在动态代理模式中它是在运行时生成的,所以你在源码甚至.class
中都找不到他的影子。
我先说结论:运行时动态生成的代理类叫做$Proxy0,关于这个类怎么看源码,很多介绍代理的文章都没有说清楚,读者也是一脸懵逼;如果你想要看它的内容,可以通过如下方法
public class Test {
public static void main(String[] args){
//生成$Proxy0的class文件
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//被代理类
Student jack =new ComputerStudent("jack");
//生成代理对象
Student jackProxy= (Student) Proxy.newProxyInstance();
}
在运行方法中添加这一行代码,在运行后会自动在项目根目录生成com文件夹,其中...\IdeaProjects\com\sun\proxy下会生成$Proxy0.class
文件。注意:我是在IDEA上调试成功的,我在Android Studio上测试是没有生成的。暂时不知道原理,有了解的大佬可以科普一下。System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
代理类$Proxy0源码如下:
package com.sun.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;
import student.Student;
public final class $Proxy0 extends Proxy implements Student {
private static Method m1;
private static Method m3;
private static Method m2;
private static Method m0;
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 exam() 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);
}
}
static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m3 = Class.forName("student.Student").getMethod("exam");
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());
}
}
}
关于$Proxy0先说几个结论:
$Proxy0
继承了Proxy,实现了自定义的目标接口Student;$Proxy0
定义了接口Student中的方法exam(),以及Object对象的几个方法equals()、toString()、hashCode()构造方法中传了 InvocationHandler
对象,并且在定义的方法中调用了它的invoke()方法
回到刚才的问题,Proxy.newProxyInstance
为什么要穿入这3个参数?
//生成代理对象
Student jackProxy= (Student) Proxy.newProxyInstance(jack.getClass().getClassLoader(),
jack.getClass().getInterfaces(),
new InvocationHandler() );
因为代理对象是基于自定义接口Student和jack类加载器代理出来的。
Q2:InvocationHandler的作用及其invoke()方法等解释
当代理对象调用方法时,会回调执行刚才new出来的InvocationHandler中的invoke()
方法。
关于invoke()方法,在看了源码和反复代码验证之后,我做出的解释如下:
/**
* 这个方法不是我们显示的去调用,是系统生成的代理类$Proxy0中调用的
*
* @param proxy 代理对象:也就是Proxy.newProxyInstance返回的对象
* @param method 要调用的方法
* @param args 调用方法的参数
* @return
* @throws Throwable
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {}
注意: invoke()
不是显示调用的,是在代理类中去调用的。比如调用exam()方法时, 该方法中会调用super.h.invoke(this, m3, null);
,就是调用父类的h的invoke(),它的父类是Proxy,h是一个InvocationHandler
对象;所以说当调用exam()方法时最后回调到刚才new出来的InvocationHandler
的invoke方法。
public final void exam() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}
下面再来看一个问题
public class Test {
public static void main(String[] args){
//生成$Proxy0的class文件
//System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
//被代理类
final Student jack =new ComputerStudent("jack");
//生成代理对象
Student jackProxy= (Student) Proxy.newProxyInstance(jack.getClass().getClassLoader(),
jack.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("before exam do something");
//通过反射调用对象的方法
method.invoke(jack,args);
System.out.println("after exam do something");
return null;
}
});
//方法调用
jackProxy.exam();
}
}
看一下InvocationHandler
中invoke()方法内部的调用
//通过反射调用对象的方法
method.invoke(jack,args);
这句代码的作用是通过反射调用一个方法,如果把实现类对象jack换成代理类对象proxy会发生什么?
结论:会循环报错,停不下来那种。因为proxy是代理实例,也就是这里的jackProxy,当这个对象的方法被调用的时候会触发InvocationHandler中invoke()方法,而InvocationHandler内部又再次调用proxy的方法,如此不停循环。
动态代理的使用场景
优点:
在运行时切入原始类,改变类的方法,这样可以丰富该方法的操作,比如在方法之前、之后做一些其它操作。
应用的话,比如Retrofit框架、AOP(面向切面编程)等等。
推荐阅读
• 955 互联网公司白名单来了!这些公司月薪20k,没有996!福利榜国内大厂只有这家!
• 基于SpringBoot 的CMS系统,拿去开发企业官网真香
• 徒手撸了一个RPC框架,理解更透彻了,代码已上传github,自取~