掃二維碼與項(xiàng)目經(jīng)理溝通
我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流
如果不出意外的話,這篇文章發(fā)出的時(shí)間是2022年2月22日的22點(diǎn)22,農(nóng)歷正月廿二,星期二。畢竟是個(gè)有點(diǎn)意思的日期,不發(fā)點(diǎn)什么總感覺(jué)有點(diǎn)浪費(fèi),畢竟我們大部分人大概率都活不到2222年的2月22日~

甘谷網(wǎng)站建設(shè)公司創(chuàng)新互聯(lián)建站,甘谷網(wǎng)站設(shè)計(jì)制作,有大型網(wǎng)站制作公司豐富經(jīng)驗(yàn)。已為甘谷上1000家提供企業(yè)網(wǎng)站建設(shè)服務(wù)。企業(yè)網(wǎng)站搭建\成都外貿(mào)網(wǎng)站建設(shè)公司要多少錢(qián),請(qǐng)找那個(gè)售后服務(wù)好的甘谷做網(wǎng)站的公司定做!
哈哈扯遠(yuǎn)了,前幾天的時(shí)候,交流群里的小伙伴拋出了一個(gè)問(wèn)題,為什么JDK的動(dòng)態(tài)代理一定要基于接口實(shí)現(xiàn)呢?
好的安排,其實(shí)要想弄懂這個(gè)問(wèn)題還是需要一些關(guān)于代理和反射的底層知識(shí)的,我們今天就盤(pán)一盤(pán)這個(gè)問(wèn)題,走你~
在分析原因之前,我們先完整的看一下實(shí)現(xiàn)jdk動(dòng)態(tài)代理需要幾個(gè)步驟,首先需要定義一個(gè)接口:
public interface Worker {
void work();
}
再寫(xiě)一個(gè)基于這個(gè)接口的實(shí)現(xiàn)類(lèi):
public class Programmer implements Worker {
@Override
public void work() {
System.out.println("coding...");
}
}
自定義一個(gè)Handler,實(shí)現(xiàn)InvocationHandler接口,通過(guò)重寫(xiě)內(nèi)部的invoke方法實(shí)現(xiàn)邏輯增強(qiáng)。其實(shí)這個(gè)InvocationHandler可以使用匿名內(nèi)部類(lèi)的形式定義,這里為了結(jié)構(gòu)清晰拿出來(lái)單獨(dú)聲明。
public class WorkHandler implements InvocationHandler {
private Object target;
WorkHandler(Object target){
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (method.getName().equals("work")) {
System.out.println("before work...");
Object result = method.invoke(target, args);
System.out.println("after work...");
return result;
}
return method.invoke(target, args);
}
}
在main方法中進(jìn)行測(cè)試,使用Proxy類(lèi)的靜態(tài)方法newProxyInstance生成一個(gè)代理對(duì)象并調(diào)用方法:
public static void main(String[] args) {
Programmer programmer = new Programmer();
Worker worker = (Worker) Proxy.newProxyInstance(
programmer.getClass().getClassLoader(),
programmer.getClass().getInterfaces(),
new WorkHandler(programmer));
worker.work();
}
執(zhí)行上面的代碼,輸出:
before work...
coding...
after work...
可以看到,執(zhí)行了方法邏輯的增強(qiáng),到這,一個(gè)簡(jiǎn)單的動(dòng)態(tài)代理過(guò)程就實(shí)現(xiàn)了,下面我們分析一下源碼。
既然是一個(gè)代理的過(guò)程,那么肯定存在原生對(duì)象和代理對(duì)象之分,下面我們查看源碼中是如何動(dòng)態(tài)的創(chuàng)建代理對(duì)象的過(guò)程。上面例子中,創(chuàng)建代理對(duì)象調(diào)用的是Proxy類(lèi)的靜態(tài)方法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() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
}//省略catch
}
概括一下上面代碼中重點(diǎn)部分:
上面這個(gè)過(guò)程中,獲取構(gòu)造方法和生成對(duì)象都是直接利用的反射,而需要重點(diǎn)看看的是生成代理類(lèi)的方法getProxyClass0。
private static Class> getProxyClass0(ClassLoader loader,
Class>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
// If the proxy class defined by the given loader implementing
// the given interfaces exists, this will simply return the cached copy;
// otherwise, it will create the proxy class via the ProxyClassFactory
return proxyClassCache.get(loader, interfaces);
}
注釋寫(xiě)的非常清晰,如果緩存中已經(jīng)存在了就直接從緩存中取,這里的proxyClassCache是一個(gè)WeakCache類(lèi)型,如果緩存中目標(biāo)classLoader和接口數(shù)組對(duì)應(yīng)的類(lèi)已經(jīng)存在,那么返回緩存的副本。如果沒(méi)有就使用ProxyClassFactory去生成Class對(duì)象。中間的調(diào)用流程可以省略,最終實(shí)際調(diào)用了ProxyClassFactory的apply方法生成Class。在apply方法中,主要做了下面3件事。
if (proxyPkg == null) {
// if no non-public proxy interfaces, use com.sun.proxy package
proxyPkg = ReflectUtil.PROXY_PACKAGE + ".";
}
/*
* Choose a name for the proxy class to generate.
*/
long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;
如果接口被定義為public公有,那么默認(rèn)會(huì)使用com.sun.proxy作為包名,類(lèi)名是$Proxy加上一個(gè)自增的整數(shù)值,初始時(shí)是0,因此生成的文件名是$Proxy0。
如果是非公有接口,那么會(huì)使用和被代理類(lèi)一樣的包名,可以寫(xiě)一個(gè)private接口的例子進(jìn)行一下測(cè)試。
package com.hydra.test.face;
public class InnerTest {
private interface InnerInterface {
void run();
}
class InnerClazz implements InnerInterface {
@Override
public void run() {
System.out.println("go");
}
}
}
這時(shí)生成的代理類(lèi)的包名為com.hydra.test.face,與被代理類(lèi)相同:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
在generateProxyClass方法中,有一個(gè)重要的參數(shù)會(huì)發(fā)揮作用:
private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));
如果這個(gè)屬性被配置為true,那么會(huì)把字節(jié)碼存儲(chǔ)到硬盤(pán)上的class文件中,否則不會(huì)保存臨時(shí)的字節(jié)碼文件。
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
返回代理類(lèi)的Class后的流程我們?cè)谇懊婢鸵呀?jīng)介紹過(guò)了,先獲得構(gòu)造方法,再使用構(gòu)造方法反射的方式創(chuàng)建代理對(duì)象。
創(chuàng)建代理對(duì)象流程的源碼分析完了,我們可以先通過(guò)debug來(lái)看看上面生成的這個(gè)代理對(duì)象究竟是個(gè)什么:
和源碼中看到的規(guī)則一樣,是一個(gè)Class為$Proxy0的神秘對(duì)象,再看一下代理對(duì)象的Class的詳細(xì)信息:
類(lèi)的全限定名是com.sun.proxy.$Proxy0,在上面我們提到過(guò),這個(gè)類(lèi)是在運(yùn)行過(guò)程中動(dòng)態(tài)生成的,并且程序執(zhí)行完成后,會(huì)自動(dòng)刪除掉class文件。如果想要保留這個(gè)臨時(shí)文件不被刪除,就要修改我們上面提到的參數(shù),具體操作起來(lái)有兩種方式,第一種是在啟動(dòng)VM參數(shù)中加入:
-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true
第二種是在代碼中加入下面這一句,注意要加在生成動(dòng)態(tài)代理對(duì)象之前:
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
使用了上面兩種方式中的任意一種后,就可以保存下來(lái)臨時(shí)的字節(jié)碼文件了,需要注意這個(gè)文件生成的位置,并不是在target目錄下,而是生成在項(xiàng)目目錄下的com\sun\proxy中,正好和默認(rèn)生成的包名對(duì)應(yīng)。
拿到字節(jié)碼文件后,就可以使用反編譯工具來(lái)反編譯它了,這里使用jad在cmd下一條命令直接搞定:
jad -s java $Proxy0.class
看一下反編譯后$Proxy0.java文件的內(nèi)容,下面的代碼中,我只保留了核心部分,省略了無(wú)關(guān)緊要的equals、toString、hashCode方法的定義。
public final class $Proxy0 extends Proxy implements Worker{
public $Proxy0(InvocationHandler invocationhandler){
super(invocationhandler);
}
public final void work(){
try{
super.h.invoke(this, m3, null);
return;
}catch(Error _ex) { }
catch(Throwable throwable){
throw new UndeclaredThrowableException(throwable);
}
}
private static Method m3;
static {
try{
m3 = Class.forName("com.hydra.test.Worker").getMethod("work", new Class[0]);
//省略其他Method
}//省略catch
}
}
這個(gè)臨時(shí)生成的代理類(lèi)$Proxy0中主要做了下面的幾件事:
到這里,整體的流程就分析完了,我們可以用一張圖來(lái)簡(jiǎn)要總結(jié)上面的過(guò)程:
通過(guò)上面的分析,我們已經(jīng)知道了代理對(duì)象是如何生成的了,那么回到開(kāi)頭的問(wèn)題,為什么jdk的動(dòng)態(tài)代理一定要基于接口呢?
其實(shí)如果不看上面的分析,我們也應(yīng)該知道,要擴(kuò)展一個(gè)類(lèi)有常見(jiàn)的兩種方式,繼承父類(lèi)或?qū)崿F(xiàn)接口。這兩種方式都允許我們對(duì)方法的邏輯進(jìn)行增強(qiáng),但現(xiàn)在不是由我們自己來(lái)重寫(xiě)方法,而是要想辦法讓jvm去調(diào)用InvocationHandler中的invoke方法,也就是說(shuō)代理類(lèi)需要和兩個(gè)東西關(guān)聯(lián)在一起:
而jdk處理這個(gè)問(wèn)題的方式是選擇繼承父類(lèi)Proxy,并把InvocationHandler存在父類(lèi)的對(duì)象中:
public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
//...
}
通過(guò)父類(lèi)Proxy的構(gòu)造方法,保存了創(chuàng)建代理對(duì)象過(guò)程中傳進(jìn)來(lái)的InvocationHandler的實(shí)例,使用protected修飾保證了它可以在子類(lèi)中被訪問(wèn)和使用。但是同時(shí),因?yàn)閖ava是單繼承的,因此在繼承了Proxy后,只能通過(guò)實(shí)現(xiàn)目標(biāo)接口的方式來(lái)實(shí)現(xiàn)方法的擴(kuò)展,達(dá)到我們?cè)鰪?qiáng)目標(biāo)方法邏輯的目的。
其實(shí)看完源碼、弄明白代理對(duì)象生成的流程后,我們還可以用另一種方法實(shí)現(xiàn)動(dòng)態(tài)代理:
public static void main(String[] args) throws Exception {
Class> proxyClass = Proxy.getProxyClass(Test3.class.getClassLoader(), Worker.class);
Constructor> constructor = proxyClass.getConstructor(InvocationHandler.class);
InvocationHandler workHandler = new WorkHandler(new Programmer());
Worker worker = (Worker) constructor.newInstance(workHandler);
worker.work();
}
運(yùn)行結(jié)果與之前相同,這種寫(xiě)法其實(shí)就是抽出了我們前面介紹的幾個(gè)核心方法,中間省略了一些參數(shù)的校驗(yàn)過(guò)程,這種方式可以幫助大家熟悉jdk動(dòng)態(tài)代理原理,但是在使用過(guò)程中還是建議大家使用標(biāo)準(zhǔn)方式,相對(duì)更加安全規(guī)范。
本文從源碼以及實(shí)驗(yàn)的角度,分析了jdk動(dòng)態(tài)代理生成代理對(duì)象的流程,通過(guò)代理類(lèi)的實(shí)現(xiàn)原理分析了為什么jdk動(dòng)態(tài)代理一定要基于接口實(shí)現(xiàn)??偟膩?lái)說(shuō),jdk動(dòng)態(tài)代理的應(yīng)用還是非常廣泛的,例如在Spring、Mybatis以及Feign等很多框架中動(dòng)態(tài)代理都被大量的使用,可以說(shuō)學(xué)好jdk動(dòng)態(tài)代理,對(duì)于我們閱讀這些框架的底層源碼還是很有幫助的。

我們?cè)谖⑿派?4小時(shí)期待你的聲音
解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流