日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
JDK動態(tài)代理為什么必須要基于接口?

如果不出意外的話,這篇文章發(fā)出的時(shí)間是2022年2月22日的22點(diǎn)22,農(nóng)歷正月廿二,星期二。畢竟是個(gè)有點(diǎn)意思的日期,不發(fā)點(diǎn)什么總感覺有點(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è)公司要多少錢,請找那個(gè)售后服務(wù)好的甘谷做網(wǎng)站的公司定做!

哈哈扯遠(yuǎn)了,前幾天的時(shí)候,交流群里的小伙伴拋出了一個(gè)問題,為什么JDK的動態(tài)代理一定要基于接口實(shí)現(xiàn)呢?

好的安排,其實(shí)要想弄懂這個(gè)問題還是需要一些關(guān)于代理和反射的底層知識的,我們今天就盤一盤這個(gè)問題,走你~

一個(gè)簡單的例子

在分析原因之前,我們先完整的看一下實(shí)現(xiàn)jdk動態(tài)代理需要幾個(gè)步驟,首先需要定義一個(gè)接口:

public interface Worker {
void work();
}

再寫一個(gè)基于這個(gè)接口的實(shí)現(xiàn)類:

public class Programmer implements Worker {
@Override
public void work() {
System.out.println("coding...");
}
}

自定義一個(gè)Handler,實(shí)現(xiàn)InvocationHandler接口,通過重寫內(nèi)部的invoke方法實(shí)現(xiàn)邏輯增強(qiáng)。其實(shí)這個(gè)InvocationHandler可以使用匿名內(nèi)部類的形式定義,這里為了結(jié)構(gòu)清晰拿出來單獨(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)行測試,使用Proxy類的靜態(tài)方法newProxyInstance生成一個(gè)代理對象并調(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è)簡單的動態(tài)代理過程就實(shí)現(xiàn)了,下面我們分析一下源碼。

Proxy源碼解析

既然是一個(gè)代理的過程,那么肯定存在原生對象和代理對象之分,下面我們查看源碼中是如何動態(tài)的創(chuàng)建代理對象的過程。上面例子中,創(chuàng)建代理對象調(diào)用的是Proxy類的靜態(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è)代理類Class或者尋找已生成過的代理類的緩存
  • 通過getConstructor方法,獲取生成的代理類的構(gòu)造方法
  • 通過newInstance方法,生成實(shí)例對象,也就是最終的代理對象

上面這個(gè)過程中,獲取構(gòu)造方法和生成對象都是直接利用的反射,而需要重點(diǎn)看看的是生成代理類的方法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);
}

注釋寫的非常清晰,如果緩存中已經(jīng)存在了就直接從緩存中取,這里的proxyClassCache是一個(gè)WeakCache類型,如果緩存中目標(biāo)classLoader和接口數(shù)組對應(yīng)的類已經(jīng)存在,那么返回緩存的副本。如果沒有就使用ProxyClassFactory去生成Class對象。中間的調(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)會使用com.sun.proxy作為包名,類名是$Proxy加上一個(gè)自增的整數(shù)值,初始時(shí)是0,因此生成的文件名是$Proxy0。

如果是非公有接口,那么會使用和被代理類一樣的包名,可以寫一個(gè)private接口的例子進(jìn)行一下測試。

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í)生成的代理類的包名為com.hydra.test.face,與被代理類相同:

  • 然后,利用ProxyGenerator.generateProxyClass方法生成代理的字節(jié)碼數(shù)組:
byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);

在generateProxyClass方法中,有一個(gè)重要的參數(shù)會發(fā)揮作用:

private static final boolean saveGeneratedFiles = (Boolean)AccessController.doPrivileged(new GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles"));

如果這個(gè)屬性被配置為true,那么會把字節(jié)碼存儲到硬盤上的class文件中,否則不會保存臨時(shí)的字節(jié)碼文件。

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

返回代理類的Class后的流程我們在前面就已經(jīng)介紹過了,先獲得構(gòu)造方法,再使用構(gòu)造方法反射的方式創(chuàng)建代理對象。

神秘的代理對象

創(chuàng)建代理對象流程的源碼分析完了,我們可以先通過debug來看看上面生成的這個(gè)代理對象究竟是個(gè)什么:

和源碼中看到的規(guī)則一樣,是一個(gè)Class為$Proxy0的神秘對象,再看一下代理對象的Class的詳細(xì)信息:

類的全限定名是com.sun.proxy.$Proxy0,在上面我們提到過,這個(gè)類是在運(yùn)行過程中動態(tài)生成的,并且程序執(zhí)行完成后,會自動刪除掉class文件。如果想要保留這個(gè)臨時(shí)文件不被刪除,就要修改我們上面提到的參數(shù),具體操作起來有兩種方式,第一種是在啟動VM參數(shù)中加入:

-Dsun.misc.ProxyGenerator.saveGeneratedFiles=true

第二種是在代碼中加入下面這一句,注意要加在生成動態(tài)代理對象之前:

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");

使用了上面兩種方式中的任意一種后,就可以保存下來臨時(shí)的字節(jié)碼文件了,需要注意這個(gè)文件生成的位置,并不是在target目錄下,而是生成在項(xiàng)目目錄下的com\sun\proxy中,正好和默認(rèn)生成的包名對應(yīng)。

拿到字節(jié)碼文件后,就可以使用反編譯工具來反編譯它了,這里使用jad在cmd下一條命令直接搞定:

jad -s java $Proxy0.class

看一下反編譯后$Proxy0.java文件的內(nèi)容,下面的代碼中,我只保留了核心部分,省略了無關(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í)生成的代理類$Proxy0中主要做了下面的幾件事:

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

到這里,整體的流程就分析完了,我們可以用一張圖來簡要總結(jié)上面的過程:

為什么要有接口?

通過上面的分析,我們已經(jīng)知道了代理對象是如何生成的了,那么回到開頭的問題,為什么jdk的動態(tài)代理一定要基于接口呢?

其實(shí)如果不看上面的分析,我們也應(yīng)該知道,要擴(kuò)展一個(gè)類有常見的兩種方式,繼承父類或?qū)崿F(xiàn)接口。這兩種方式都允許我們對方法的邏輯進(jìn)行增強(qiáng),但現(xiàn)在不是由我們自己來重寫方法,而是要想辦法讓jvm去調(diào)用InvocationHandler中的invoke方法,也就是說代理類需要和兩個(gè)東西關(guān)聯(lián)在一起:

  • 被代理類
  • InvocationHandler

而jdk處理這個(gè)問題的方式是選擇繼承父類Proxy,并把InvocationHandler存在父類的對象中:

public class Proxy implements java.io.Serializable {
protected InvocationHandler h;
protected Proxy(InvocationHandler h) {
Objects.requireNonNull(h);
this.h = h;
}
//...
}

通過父類Proxy的構(gòu)造方法,保存了創(chuàng)建代理對象過程中傳進(jìn)來的InvocationHandler的實(shí)例,使用protected修飾保證了它可以在子類中被訪問和使用。但是同時(shí),因?yàn)閖ava是單繼承的,因此在繼承了Proxy后,只能通過實(shí)現(xiàn)目標(biāo)接口的方式來實(shí)現(xiàn)方法的擴(kuò)展,達(dá)到我們增強(qiáng)目標(biāo)方法邏輯的目的。

扯點(diǎn)別的

其實(shí)看完源碼、弄明白代理對象生成的流程后,我們還可以用另一種方法實(shí)現(xiàn)動態(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é)果與之前相同,這種寫法其實(shí)就是抽出了我們前面介紹的幾個(gè)核心方法,中間省略了一些參數(shù)的校驗(yàn)過程,這種方式可以幫助大家熟悉jdk動態(tài)代理原理,但是在使用過程中還是建議大家使用標(biāo)準(zhǔn)方式,相對更加安全規(guī)范。

總結(jié)

本文從源碼以及實(shí)驗(yàn)的角度,分析了jdk動態(tài)代理生成代理對象的流程,通過代理類的實(shí)現(xiàn)原理分析了為什么jdk動態(tài)代理一定要基于接口實(shí)現(xiàn)??偟膩碚f,jdk動態(tài)代理的應(yīng)用還是非常廣泛的,例如在Spring、Mybatis以及Feign等很多框架中動態(tài)代理都被大量的使用,可以說學(xué)好jdk動態(tài)代理,對于我們閱讀這些框架的底層源碼還是很有幫助的。


網(wǎng)頁題目:JDK動態(tài)代理為什么必須要基于接口?
文章轉(zhuǎn)載:http://www.5511xx.com/article/djioese.html