av激情亚洲男人的天堂国语,日韩欧美精品一中文字幕,无码av一区二区三区无码,国产又色又爽又刺激的a片,国产又色又爽又刺激的a片

JDK動(dòng)態(tài)代理為什么必須要基于接口?

如果不出意外的話,這篇文章發(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)題,走你~

一個(gè)簡(jiǎ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)了,下面我們分析一下源碼。

Proxy源碼解析

既然是一個(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)部分:

  • 在checkProxyAccess方法中,進(jìn)行參數(shù)驗(yàn)證
  • 在getProxyClass0方法中,生成一個(gè)代理類(lèi)Class或者尋找已生成過(guò)的代理類(lèi)的緩存
  • 通過(guò)getConstructor方法,獲取生成的代理類(lèi)的構(gòu)造方法
  • 通過(guò)newInstance方法,生成實(shí)例對(duì)象,也就是最終的代理對(duì)象

上面這個(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件事。

  • 首先,根據(jù)規(guī)則生成文件名:
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)相同:

  • 然后,利用ProxyGenerator.generateProxyClass方法生成代理的字節(jié)碼數(shù)組:
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é)碼文件。

  • 最后,調(diào)用本地方法defineClass0生成Class對(duì)象:
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);

返回代理類(lèi)的Class后的流程我們?cè)谇懊婢鸵呀?jīng)介紹過(guò)了,先獲得構(gòu)造方法,再使用構(gòu)造方法反射的方式創(chuàng)建代理對(duì)象。

神秘的代理對(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中主要做了下面的幾件事:

  • 在這個(gè)類(lèi)的靜態(tài)代碼塊中,通過(guò)反射初始化了多個(gè)靜態(tài)方法Method變量,除了接口中的方法還有equals、toString、hashCode這三個(gè)方法
  • 繼承父類(lèi)Proxy,實(shí)例化的過(guò)程中會(huì)調(diào)用父類(lèi)的構(gòu)造方法,構(gòu)造方法中傳入的invocationHandler對(duì)象實(shí)際上就是我們自定義的WorkHandler的實(shí)例
  • 實(shí)現(xiàn)了自定義的接口Worker,并重寫(xiě)了work方法,方法內(nèi)調(diào)用了InvocationHandler的invoke方法,也就是實(shí)際上調(diào)用了WorkHandler的invoke方法
  • 省略的equals、toString、hashCode方法實(shí)現(xiàn)也一樣,都是調(diào)用super.h.invoke()方法

到這里,整體的流程就分析完了,我們可以用一張圖來(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)在一起:

  • 被代理類(lèi)
  • InvocationHandler

而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)方法邏輯的目的。

扯點(diǎn)別的

其實(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ī)范。

總結(jié)

本文從源碼以及實(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ì)于我們閱讀這些框架的底層源碼還是很有幫助的。


當(dāng)前題目:JDK動(dòng)態(tài)代理為什么必須要基于接口?
分享鏈接:http://uogjgqi.cn/article/djioese.html
掃二維碼與項(xiàng)目經(jīng)理溝通

我們?cè)谖⑿派?4小時(shí)期待你的聲音

解答本文疑問(wèn)/技術(shù)咨詢(xún)/運(yùn)營(yíng)咨詢(xún)/技術(shù)建議/互聯(lián)網(wǎng)交流