自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

原來這才是動態(tài)代理?。。?/h1>

開發(fā) 架構(gòu)
動態(tài)代理的應用場景有很多,最常見的就是 AOP 的實現(xiàn)、RPC 遠程調(diào)用、Java 注解對象獲取、日志框架、全局性異常處理、事務處理等。

各位小伙伴們大家吼啊!我是 cxuan,距離上次更新已經(jīng)有段時間了,臨近過年了,項目這邊也比較忙,而且最近很多時間都花在看書、提升自己上面,文章寫的比較拖沓,這里我要自我反思(其實我已經(jīng)籌備了幾篇文章,就等結(jié)尾了,嘿嘿嘿)。

我們上篇文章聊了一波什么是動態(tài)代理,然后我又從動態(tài)代理的四種實現(xiàn)為切入點,為你講解 JDK 動態(tài)代理、CGLIB 動態(tài)代理、Javaassist、ASM 反向生成字節(jié)碼的區(qū)別,具體的內(nèi)容你可以參見下面這篇文章。

動態(tài)代理竟然如此簡單!

那么這篇文章我們來聊一下動態(tài)代理的實現(xiàn)原理。

為了保險起見,我們首先花幾分鐘回顧一下什么是動態(tài)代理吧!

什么是動態(tài)代理

首先,動態(tài)代理是代理模式的一種實現(xiàn)方式,代理模式除了動態(tài)代理還有 靜態(tài)代理,只不過靜態(tài)代理能夠在編譯時期確定類的執(zhí)行對象,而動態(tài)代理只有在運行時才能夠確定執(zhí)行對象是誰。代理可以看作是對最終調(diào)用目標的一個封裝,我們能夠通過操作代理對象來調(diào)用目標類,這樣就可以實現(xiàn)調(diào)用者和目標對象的解耦合。

動態(tài)代理的應用場景有很多,最常見的就是 AOP 的實現(xiàn)、RPC 遠程調(diào)用、Java 注解對象獲取、日志框架、全局性異常處理、事務處理等。

動態(tài)代理的實現(xiàn)有很多,但是 JDK 動態(tài)代理是很重要的一種,下面我們就 JDK 動態(tài)代理來深入理解一波。

JDK 動態(tài)代理

首先我們先來看一下動態(tài)代理的執(zhí)行過程

在 JDK 動態(tài)代理中,實現(xiàn)了 InvocationHandler的類可以看作是 代理類(因為類也是一種對象,所以我們上面為了描述關系,把代理類形容成了代理對象)。JDK 動態(tài)代理就是圍繞實現(xiàn)了 InvocationHandler 的代理類進行的,比如下面就是一個 InvocationHandler 的實現(xiàn)類,同時它也是一個代理類。

public class UserHandler implements InvocationHandler {

private UserDao userDao;

public UserHandler(UserDao userDao){
this.userDao = userDao;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
saveUserStart();
Object obj = method.invoke(userDao, args);
saveUserDone();
return obj;
}

public void saveUserStart(){
System.out.println("---- 開始插入 ----");
}

public void saveUserDone(){
System.out.println("---- 插入完成 ----");
}
}

代理類一個最最最重要的方法就是 invoke 方法,它有三個參數(shù)

  • Object proxy: 動態(tài)代理對象,關于這個方法我們后面會說。
  • Method method: 表示最終要執(zhí)行的方法,method.invoke 用于執(zhí)行被代理的方法,也就是真正的目標方法
  • Object[] args: 這個參數(shù)就是向目標方法傳遞的參數(shù)。

我們構(gòu)造好了代理類,現(xiàn)在就要使用它來實現(xiàn)我們對目標對象的調(diào)用,那么如何操作呢?請看下面代碼

public static void dynamicProxy(){

UserDao userDao = new UserDaoImpl();
InvocationHandler handler = new UserHandler(userDao);

ClassLoader loader = userDao.getClass().getClassLoader();
Class<?>[] interfaces = userDao.getClass().getInterfaces();

UserDao proxy = (UserDao)Proxy.newProxyInstance(loader, interfaces, handler);
proxy.saveUser();
}

如果要用 JDK 動態(tài)代理的話,就需要知道目標對象的類加載器、目標對象的接口,當然還要知道目標對象是誰。構(gòu)造完成后,我們就可以調(diào)用 Proxy.newProxyInstance方法,然后把類加載器、目標對象的接口、目標對象綁定上去就完事兒了。

這里需要注意一下 Proxy 類,它就是動態(tài)代理實現(xiàn)所用到的代理類。

Proxy 位于java.lang.reflect 包下,這同時也旁敲側(cè)擊的表明動態(tài)代理的本質(zhì)就是反射。

下面我們就圍繞 JDK 動態(tài)代理,來深入理解一下它的原理,以及搞懂為什么動態(tài)代理的本質(zhì)就是反射。

動態(tài)代理的實現(xiàn)原理

在了解動態(tài)代理的實現(xiàn)原理之前,我們先來了解一下 InvocationHandler 接口

InvocationHandler 接口

JavaDoc 告訴我們,InvocationHandler 是一個接口,實現(xiàn)這個接口的類就表示該類是一個代理實現(xiàn)類,也就是代理類。

InvocationHandler 接口中只有一個 invoke 方法。

動態(tài)代理的優(yōu)勢在于能夠很方便的對代理類中方法進行集中處理,而不用修改每個被代理的方法。因為所有被代理的方法(真正執(zhí)行的方法)都是通過在 InvocationHandler 中的 invoke 方法調(diào)用的。所以我們只需要對 invoke 方法進行集中處理。

invoke 方法只有三個參數(shù)

public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
  • proxy:代理對象
  • method: 代理對象調(diào)用的方法
  • args:調(diào)用方法中的參數(shù)。

動態(tài)代理的整個代理過程不像靜態(tài)代理那樣一目了然,清晰易懂,因為在動態(tài)代理的過程中,我們沒有看到代理類的真正代理過程,也不明白其具體操作,所以要分析動態(tài)代理的實現(xiàn)原理,我們必須借助源碼。

那么問題來了,首先第一步應該從哪分析?如果不知道如何分析的話,干脆就使用倒推法,從后往前找,我們直接先從 Proxy.newProxyInstance入手,看看是否能略知一二。

Proxy.newInstance 方法分析

Proxy 提供了創(chuàng)建動態(tài)代理類和實例的靜態(tài)方法,它也是由這些方法創(chuàng)建的所有動態(tài)代理類的超類。

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);
}

Class<?> cl = getProxyClass0(loader, intfs);

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;
}
});
}
return cons.newInstance(new Object[]{h});
} catch (Exception e) {
...
}

乍一看起來有點麻煩,其實源碼都是這樣,看起來非常復雜,但是慢慢分析、厘清條理過后就好,最重要的是分析源碼不能著急。

上面這個 Proxy.newProxyInstsance 其實就做了下面幾件事,我畫了一個流程圖作為參考。

從上圖中我們也可以看出,newProxyInstsance 方法最重要的幾個環(huán)節(jié)就是獲得代理類、獲得構(gòu)造器,然后構(gòu)造新實例。

對反射有一些了解的同學,應該會知道獲得構(gòu)造器和構(gòu)造新實例是怎么回事。關于反射,可以參考筆者的這篇文章

學會反射后,我被錄取了!

所以我們的重點就放在了獲得代理類,這是最關鍵的一步,對應源碼中的 Class cl = getProxyClass0(loader, intfs); 我們進入這個方法一探究竟

private static Class<?> getProxyClass0(ClassLoader loader,
Class<?>... interfaces) {
if (interfaces.length > 65535) {
throw new IllegalArgumentException("interface limit exceeded");
}
return proxyClassCache.get(loader, interfaces);
}

這個方法比較簡單,首先會直接判斷接口長度是否大于 65535(剛開始看到這里我是有點不明白的,我心想這個判斷是要判斷什么?interfaces 這不是一個 class 類型嗎,從 length 點進去也看不到這個屬性,細看一下才明白,這居然是可變參數(shù),Class ... 中的 ... 就是可變參數(shù),所以這個判斷我猜測應該是判斷接口數(shù)量是否大于 65535。)

然后會直接從 proxyClassCache 中根據(jù) loader 和 interfaces 獲取代理對象實例。如果能夠根據(jù) loader 和 interfaces 找到代理對象,將會返回緩存中的對象副本;否則,它將通過 ProxyClassFactory 創(chuàng)建代理類。

proxyClassCache.get 就是一系列從緩存中的查詢操作,注意這里的 proxyClassCache 其實是一個WeakCache,WeakCahe 也是位于 java.lang.reflect 包下的一個緩存映射 map,它的主要特點是一個弱引用的 map,但是它內(nèi)部有一個 SubKey ,這個子鍵卻是強引用的。

這里我們不用去追究這個 proxyClassCache 是如何進行緩存的,只需要知道它的緩存時機就可以了:即在類加載的時候進行緩存。

如果無法找到代理對象,就會通過 ProxyClassFactory 創(chuàng)建代理,ProxyClassFactory 繼承于 BiFunction

private static final class ProxyClassFactory
implements BiFunction<ClassLoader, Class<?>[], Class<?>>
{...}

ProxyClassFactory 里面有兩個屬性一個方法。

proxyClassNamePrefix:這個屬性表明使用 ProxyClassFactory 創(chuàng)建出來的代理實例的命名是以 "$Proxy" 為前綴的。

nextUniqueNumber:這個屬性表明 ProxyClassFactory 的后綴是使用 AtomicLong 生成的數(shù)字

所以代理實例的命名一般是 、Proxy1這種。

這個 apply 方法是一個根據(jù)接口和類加載器進行代理實例創(chuàng)建的工廠方法,下面是這段代碼的核心。

@Override
public Class<?> apply(ClassLoader loader, Class<?>[] interfaces) {

...

long num = nextUniqueNumber.getAndIncrement();
String proxyName = proxyPkg + proxyClassNamePrefix + num;

byte[] proxyClassFile = ProxyGenerator.generateProxyClass(
proxyName, interfaces, accessFlags);
try {
return defineClass0(loader, proxyName,
proxyClassFile, 0, proxyClassFile.length);
} catch (ClassFormatError e) {
throw new IllegalArgumentException(e.toString());
}
}

可以看到,代理實例的命名就是我們上面所描述的那種命名方式,只不過它這里加上了 proxyPkg 包名的路徑。然后下面就是生成代理實例的關鍵代碼。

ProxyGenerator.generateProxyClass 我們跟進去是只能看到 .class 文件的,class 文件是虛擬機編譯之后的結(jié)果,所以我們要看一下 .java 文件源碼。.java 源碼位于 OpenJDK中的 sun.misc 包中的 ProxyGenerator 下。

此類的 generateProxyClass() 靜態(tài)方法的核心內(nèi)容就是去調(diào)用 generateClassFile() 實例方法來生成 Class 文件。方法太長了我們不貼了,這里就大致解釋以下其作用:

  • 第一步:收集所有要生成的代理方法,將其包裝成 ProxyMethod 對象并注冊到 Map 集合中。
  • 第二步:收集所有要為 Class 文件生成的字段信息和方法信息。
  • 第三步:完成了上面的工作后,開始組裝 Class 文件。

而 defineClass0 這個方法我們點進去是 native ,底層是 C/C++ 實現(xiàn)的,于是我又去看了一下 C/C++ 源碼,路徑在

點開之后的 C/C++ 源碼還是挺讓人絕望的。

不過我們再回頭看一下這個 defineClass0 方法,它實際上就是根據(jù)上面生成的 proxyClassFile 字節(jié)數(shù)組來生成對應的實例罷了,所以我們不必再深究 C/C++ 對于代理對象的合成過程了。

所以總結(jié)一下可以看出,JDK 為我們的生成了一個叫 $Proxy0 的代理類,這個類文件放在內(nèi)存中的,我們在創(chuàng)建代理對象時,就是通過反射獲得這個類的構(gòu)造方法,然后創(chuàng)建的代理實例。

所以最開始的 dynamicProxy 方法我們反編譯后的代碼就是這樣的

public final class $Proxy0 extends java.lang.reflect.Proxy implements com.cxuan.dynamic.UserDao {
public $Proxy0(java.lang.reflect.InvocationHandler) throws ;
Code:
0: aload_0
1: aload_1
2: invokespecial #8 // Method java/lang/reflect/Proxy."<init>":(Ljava/lang/reflect/InvocationHandler;)V
5: return

我們看到代理類繼承了 Proxy 類,所以也就決定了 Java 動態(tài)代理只能對接口進行代理。

于是,上面這個圖你應該就可以看懂了。

invoke 方法中第一個參數(shù) proxy 的作用

細心的小伙伴們可能都發(fā)現(xiàn)了,invoke 方法中第一個 proxy 的作用是啥?代碼里面好像 proxy 也沒用到啊,這個參數(shù)的意義是啥呢?它運行時的類型是啥啊?為什么不使用 this 代替呢?

Stackoverflow 給出了我們一個回答 https://stackoverflow.com/questions/22930195/understanding-proxy-arguments-of-the-invoke-method-of-java-lang-reflect-invoca

什么意思呢?

就是說這個 proxy ,它是真正的代理對象,invoke 方法可以返回調(diào)用代理對象方法的返回結(jié)果,也可以返回對象的真實代理對象,也就是 $Proxy0,這也是它運行時的類型。

至于為什么不用 this 來代替 proxy,因為實現(xiàn)了 InvocationHandler 的對象中的 this ,指代的還是 InvocationHandler 接口實現(xiàn)類本身,而不是真實的代理對象。

后記

另外,這段時間公眾號出了一些狀況,大家在公眾號回復的一些關鍵詞都沒有對應的連接了,這里和大家說明抱歉。


責任編輯:武曉燕 來源: 程序員cxua
相關推薦

2021-12-15 07:24:56

SocketTCPUDP

2024-06-03 09:52:08

2015-11-16 13:31:24

大數(shù)據(jù)騙局

2019-05-05 09:24:09

KafkaTopicPartition

2016-02-29 10:52:02

大數(shù)據(jù)數(shù)據(jù)基礎設施大數(shù)據(jù)應用

2021-06-21 09:36:44

微信語音轉(zhuǎn)發(fā)

2022-03-16 12:30:27

云服務器服務器

2013-11-28 14:34:30

微軟WP

2024-09-25 08:22:06

2020-05-28 10:45:31

Git分支合并

2016-12-16 19:06:02

擴展數(shù)據(jù)庫架構(gòu)

2012-05-17 11:04:18

匈牙利命名法

2022-04-26 18:08:21

C語言代碼編程規(guī)范

2021-02-28 13:52:46

程序員編碼技術

2017-03-08 13:12:44

編程學習

2024-12-06 12:17:31

2020-03-05 16:47:51

Git內(nèi)部儲存

2015-10-14 09:17:44

大可樂手機破產(chǎn)

2019-01-02 10:49:54

Tomcat內(nèi)存HotSpot VM

2015-02-11 09:35:09

iPhone6
點贊
收藏

51CTO技術棧公眾號