當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > 架構(gòu)師社區(qū)
[導(dǎo)讀]這是一個(gè)困擾我司由來(lái)已久的難題,Dubbo 了解過(guò)吧,對(duì)外提供的服務(wù)可能有多個(gè)方法,一般我們?yōu)榱瞬唤o調(diào)用方埋坑。


漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

這是一個(gè)困擾我司由來(lái)已久的難題,Dubbo 了解過(guò)吧,對(duì)外提供的服務(wù)可能有多個(gè)方法,一般我們?yōu)榱瞬唤o調(diào)用方埋坑,會(huì)在每個(gè)方法里把所有異常都 catch 住,只返回一個(gè) result,調(diào)用方會(huì)根據(jù)這個(gè) result 里的 success 判斷此次調(diào)用是否成功,舉個(gè)例子

public class ServiceResultTO<T> extends Serializable { private static final long serialVersionUID = xxx; private Boolean success; private String message; private T data;
} public interface TestService { ServiceResultTOtest();
} public class TestServiceImpl implements TestService { @Override public ServiceResultTOtest() { try { // 此處寫(xiě)服務(wù)里的執(zhí)行邏輯 return ServiceResultTO.buildSuccess(Boolean.TRUE);
       } catch(Exception e) { return ServiceResultTO.buildFailed(Boolean.FALSE, "執(zhí)行失敗");            
       }
   }
}

比如現(xiàn)在以上這樣的 dubbo 服務(wù)(TestService),它有一個(gè) test 方法,為了執(zhí)行正常邏輯時(shí)出現(xiàn)異常,我們?cè)诖朔椒▓?zhí)行邏輯外包了一層「try... catch...」如果只有一個(gè) test 方法,這樣做當(dāng)然沒(méi)問(wèn)題,但問(wèn)題是在工程里我們一般要要提供幾十上百個(gè) service,每個(gè) service 有幾十個(gè)像 test 這樣的方法,如果每個(gè)方法都要在執(zhí)行的時(shí)候包一層 「try ...catch...」,雖然可行,但代碼會(huì)比較丑陋,可讀性也比較差,你能想想辦法改進(jìn)一下嗎?

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

既然是用切面解決的,我先解釋下什么是切面。我們知道,面向?qū)ο髮⒊绦虺橄蟪啥鄠€(gè)層次的對(duì)象,每個(gè)對(duì)象負(fù)責(zé)不同的模塊,這樣的話(huà)各個(gè)對(duì)象分工明確,各司其職,也不互相藕合,確實(shí)有力地促進(jìn)了工程開(kāi)發(fā)與分工協(xié)作,但是新的問(wèn)題來(lái)了,不同的模塊(對(duì)象)間有時(shí)會(huì)出現(xiàn)公共的行為,這種公共的行為很難通過(guò)繼承的方式來(lái)實(shí)現(xiàn),如果用工具類(lèi)的話(huà)也不利于維護(hù),代碼也顯得異常繁瑣。切面(AOP)的引入就是為了解決這類(lèi)問(wèn)題而生的,它要達(dá)到的效果是保證開(kāi)發(fā)者在不修改源代碼的前提下,為系統(tǒng)中不同的業(yè)務(wù)組件添加某些通用功能。

漫畫(huà):AOP 面試造火箭事件始末

舉個(gè)例子來(lái)說(shuō)說(shuō)

漫畫(huà):AOP 面試造火箭事件始末

漫畫(huà):AOP 面試造火箭事件始末比如上面這個(gè)例子,三個(gè) service 對(duì)象執(zhí)行過(guò)程中都存在安全,事務(wù),緩存,性能等相同行為,這些相同的行為顯然應(yīng)該在同一個(gè)地方管理,有人說(shuō)我可以寫(xiě)一個(gè)統(tǒng)一的工具類(lèi),在這些對(duì)象的方法前/后都嵌入此工具類(lèi),那問(wèn)題來(lái)了,這些行為都屬于業(yè)務(wù)無(wú)關(guān)的,使用工具類(lèi)嵌入的方式導(dǎo)致與業(yè)務(wù)代碼緊藕合,很不合工程規(guī)范,代碼可維護(hù)性極差!切面就是為了解決此類(lèi)問(wèn)題應(yīng)運(yùn)而生的,能做到相同功能的統(tǒng)一管理,對(duì)業(yè)務(wù)代碼無(wú)侵入

漫畫(huà):AOP 面試造火箭事件始末

以性能為例,這些對(duì)象負(fù)責(zé)的模塊存在哪些相似的功能呢

漫畫(huà):AOP 面試造火箭事件始末

比如說(shuō)吧,每個(gè) service 都有不同的方法,我想統(tǒng)計(jì)每個(gè)方法的執(zhí)行時(shí)間,如果不用切面你需要在每個(gè)方法的首尾計(jì)算下時(shí)間,然后相減

漫畫(huà):AOP 面試造火箭事件始末

如果我要統(tǒng)計(jì)每一個(gè) service 中每個(gè)方法的執(zhí)行時(shí)間可想而知不用切面的話(huà)就得在每個(gè)方法的首尾都加上類(lèi)似上述的邏輯,顯然這樣的代碼可維護(hù)性是非常差的,這還只是統(tǒng)計(jì)時(shí)間,如果此方法又要加上事務(wù),風(fēng)控等,是不是也得在方法首尾加上事務(wù)開(kāi)始,回滾等代碼,可想而知業(yè)務(wù)代碼與非業(yè)務(wù)代碼嚴(yán)重藕合,這樣的實(shí)現(xiàn)方式對(duì)工程是一種災(zāi)難,是不能接受的!

漫畫(huà):AOP 面試造火箭事件始末

那如果用切面該怎么做呢

漫畫(huà):AOP 面試造火箭事件始末

在說(shuō)解決方案前,首先我們要看下與切面相關(guān)的幾個(gè)定義漫畫(huà):AOP 面試造火箭事件始末JoinPoint: 程序在執(zhí)行流程中經(jīng)過(guò)的一個(gè)個(gè)時(shí)間點(diǎn),這個(gè)時(shí)間點(diǎn)可以是方法調(diào)用時(shí),或者是執(zhí)行方法中異常拋出時(shí),也可以是屬性被修改時(shí)等時(shí)機(jī),在這些時(shí)間點(diǎn)上你的切面代碼是可以(注意是可以但未必)被注入的

Pointcut: JoinPoints 只是切面代碼可以被織入的地方,但我并不想對(duì)所有的 JoinPoint 進(jìn)行織入,這就需要某些條件來(lái)篩選出那些需要被織入的 JoinPoint,Pointcut 就是通過(guò)一組規(guī)則(使用 AspectJ pointcut expression language 來(lái)描述) 來(lái)定位到匹配的 joinpoint

Advice:  代碼織入(也叫增強(qiáng)),Pointcut 通過(guò)其規(guī)則指定了哪些 joinpoint 可以被織入,而 Advice 則指定了這些 joinpoint 被織入(或者增強(qiáng))的具體時(shí)機(jī)與邏輯,是切面代碼真正被執(zhí)行的地方,主要有五個(gè)織入時(shí)機(jī)

  1. Before Advice: 在 JoinPoints 執(zhí)行前織入
  2. After Advice: 在 JoinPoints 執(zhí)行后織入(不管是否拋出異常都會(huì)織入)
  3. After returning advice: 在 JoinPoints 執(zhí)行正常退出后織入(拋出異常則不會(huì)被織入)
  4. After throwing advice: 方法執(zhí)行過(guò)程中拋出異常后織入
  5. Around Advice: 這是所有 Advice 中最強(qiáng)大的,它在 JoinPoints 前后都可織入切面代碼,也可以選擇是否執(zhí)行原有正常的邏輯,如果不執(zhí)行原有流程,它甚至可以用自己的返回值代替原有的返回值,甚至拋出異常。在這些 advice 里我們就可以寫(xiě)入切面代碼了 綜上所述,切面(Aspect)我們可以認(rèn)為就是 pointcut 和 advice,pointcut 指定了哪些 joinpoint 可以被織入,而 advice 則指定了在這些 joinpoint 上的代碼織入時(shí)機(jī)與邏輯
畫(huà)外音:織入(weaving),將切面作用于委托類(lèi)對(duì)象以創(chuàng)建 adviced object 的過(guò)程(即代理,下文會(huì)提)
漫畫(huà):AOP 面試造火箭事件始末

列了一大堆概念真讓人生氣,請(qǐng)用你奶奶都能聽(tīng)得懂的語(yǔ)言來(lái)解釋一下這些概念!

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

把技術(shù)解釋得讓非技術(shù)的人也聽(tīng)懂才叫本事,這才說(shuō)明你真的懂了。

漫畫(huà):AOP 面試造火箭事件始末

這也難不倒我,比如在餐館里點(diǎn)菜,菜單有 10 個(gè)菜,這 10 個(gè)菜就是 JoinPoint,但我只點(diǎn)了帶有蘿卜名字的菜,那么帶有蘿卜名字這個(gè)條件就是針對(duì) JoinPoint(10 個(gè)菜)的篩選條件,即 pointcut,最終只有胡蘿卜,白蘿卜這兩個(gè) JoinPoint 滿(mǎn)足條件,然后我們就可以在吃胡蘿卜前洗手(before advice),或吃胡蘿卜后買(mǎi)單(after advice),也可以統(tǒng)計(jì)吃胡蘿卜的時(shí)間(around advice),這些洗手,買(mǎi)單,統(tǒng)計(jì)時(shí)間的動(dòng)作都是與吃蘿卜這個(gè)業(yè)務(wù)動(dòng)作解藕的,都是統(tǒng)一寫(xiě)在 advice 的邏輯里

漫畫(huà):AOP 面試造火箭事件始末

能否用程序?qū)崿F(xiàn)一下,talk is cheap, show me your code!

漫畫(huà):AOP 面試造火箭事件始末

好嘞,讓你看下我的實(shí)力

public interface TestService { // 吃蘿卜 void eatCarrot(); // 吃蘑菇 void eatMushroom(); // 吃白菜 void eatCabbage();
} @Component public class TestServiceImpl implements TestService { @Override public void eatCarrot() {
       System.out.println("吃蘿卜");
   } @Override public void eatMushroom() {
       System.out.println("吃蘑菇");
   } @Override public void eatCabbage() {
       System.out.println("吃白菜");
   }
}

假設(shè)有以上 TestService, 實(shí)現(xiàn)了吃蘿卜,吃蘑菇,吃白菜三個(gè)方法,這三個(gè)方法都用切面織入,所以它們都是 joinpoints,但現(xiàn)在我只想對(duì)吃蘿卜這個(gè) joinpoints 前后織入 advice,該怎么辦呢,首先當(dāng)然要聲明 pointcut 表達(dá)式,這個(gè)表達(dá)式表明只想織入吃蘿卜這個(gè) joinpoint,指明了之后再讓 advice 應(yīng)用于此 pointcut 不就完了,比如我想在吃蘿卜前洗手,吃蘿卜后買(mǎi)單,可以寫(xiě)出如下切面邏輯

@Aspect @Component public class TestAdvice { // 1. 定義 PointCut @Pointcut("execution(* com.example.demo.api.TestServiceImpl.eatCarrot())") private void eatCarrot(){} // 2. 定義應(yīng)用于 JoinPoint 中所有滿(mǎn)足 PointCut 條件的 advice, 這里我們使用 around advice,在其中織入增強(qiáng)邏輯 @Around("eatCarrot()") public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable {
       System.out.println("吃蘿卜前洗手"); //  原來(lái)的 TestServiceImpl.eatCarrot 邏輯,可視情況決定是否執(zhí)行 point.proceed();
       System.out.println("吃蘿后買(mǎi)單");
   }
}

可以看到通過(guò) AOP 我們巧妙地在方法執(zhí)行前后執(zhí)行插入相關(guān)的邏輯,對(duì)原有執(zhí)行邏輯無(wú)任何侵入!

漫畫(huà):AOP 面試造火箭事件始末

小子果然有兩把刷子,我們 HR 眼光不錯(cuò),還有一個(gè)問(wèn)題,開(kāi)頭我司的那個(gè)難題你用切面又是如何解決的呢。

漫畫(huà):AOP 面試造火箭事件始末

這就要說(shuō)到 PointCut 的 AspectJ pointcut expression language 聲明式表達(dá)式,這個(gè)表達(dá)式支持的類(lèi)型比較全面,可以用正則,注解等來(lái)指定滿(mǎn)足條件的 joinpoint , 比如類(lèi)名后加 .*(..) 這樣的正則表達(dá)式就代表這個(gè)類(lèi)里面的所有方法都會(huì)被織入,使用 @annotation 的方式也可以指定對(duì)標(biāo)有這類(lèi)注解的方法織入代碼

漫畫(huà):AOP 面試造火箭事件始末

恩,可以,繼續(xù)

漫畫(huà):AOP 面試造火箭事件始末

首先我們先定義一個(gè)如下注解

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface GlobalErrorCatch {

}

然后將所有 service 中方法里的 「try... catch...」移除掉,在方法簽名上加上上述我們定義好的注解

public class TestServiceImpl implements TestService { @Override @GlobalErrorCatch public ServiceResultTOtest() { // 此處寫(xiě)服務(wù)里的執(zhí)行邏輯 boolean result = xxx; return ServiceResultTO.buildSuccess(result);
   }
}

然后再指定注解形式的 pointcuts 及 around advice

@Aspect @Component public class TestAdvice { // 1. 定義所有帶有 GlobalErrorCatch 的注解的方法為 Pointcut @Pointcut("@annotation(com.example.demo.annotation.GlobalErrorCatch)") private void globalCatch(){} // 2. 將 around advice 作用于 globalCatch(){} 此 PointCut  @Around("globalCatch()") public Object handlerGlobalResult(ProceedingJoinPoint point) throws Throwable { try { return point.proceed();
       } catch (Exception e) {
           System.out.println("執(zhí)行錯(cuò)誤" + e); return ServiceResultTO.buildFailed("系統(tǒng)錯(cuò)誤");
       }
   }

}

通過(guò)這樣的方式,所有標(biāo)記著 GlobalErrorCatch 注解的方法都會(huì)統(tǒng)一在 handlerGlobalResult 方法里執(zhí)行,我們就可以在這個(gè)方法里統(tǒng)一 catch 住異常,所有 service 方法中又長(zhǎng)又臭的 「try...catch...」全部干掉,真香!

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

按照大佬提供的思路,我首先打印了 TestServiceImp 這個(gè) bean 所屬的類(lèi)

@Component public class TestServiceImpl implements TestService { @Override public void eatCarrot() {
       System.out.println("吃蘿卜");
   }
} @Aspect @Component public class TestAdvice { // 1. 定義 PointCut @Pointcut("execution(* com.example.demo.api.TestServiceImpl.eatCarrot())") private void eatCarrot(){} // 2. 定義應(yīng)用于 PointCut 的 advice, 這里我們使用 around advice @Around("eatCarrot()") public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable { // 省略相關(guān)邏輯 }
} @SpringBootApplication @EnableAspectJAutoProxy public class DemoApplication { public static void main(String[] args) {
       ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
       TestService testService = context.getBean(TestService.class);
       System.out.println("testService = " + testService.getClass());
   }
}

漫畫(huà):AOP 面試造火箭事件始末打印后我果然發(fā)現(xiàn)了端倪,這個(gè) bean 的 class 居然不是 TestServiceImpl!而是com.example.demo.impl.TestServiceImpl EnhancerBySpringCGLIB$$705c68c7!

漫畫(huà):AOP 面試造火箭事件始末

果然有長(zhǎng)進(jìn),繼續(xù)說(shuō),為啥會(huì)生成這樣一個(gè)類(lèi)

漫畫(huà):AOP 面試造火箭事件始末

我們注意到類(lèi)名中有一個(gè) EnhancerBySpringCGLIB ,注意 CGLiB,這個(gè)類(lèi)就是通過(guò)它生成的動(dòng)態(tài)代理

漫畫(huà):AOP 面試造火箭事件始末

打住,先不要說(shuō)動(dòng)態(tài)代理,先談?wù)勆妒谴戆?

漫畫(huà):AOP 面試造火箭事件始末

代理在生活中隨處可見(jiàn),比如說(shuō)我要買(mǎi)房,我一般不會(huì)直接和賣(mài)家對(duì)接,一般會(huì)和中介打交道,中介就是代理,賣(mài)家就是目標(biāo)對(duì)象,我就是調(diào)用者,代理不僅實(shí)現(xiàn)了目標(biāo)對(duì)象的行為(幫目標(biāo)對(duì)象賣(mài)房),還可以添加上自己的動(dòng)作(收保證金,簽合同等),漫畫(huà):AOP 面試造火箭事件始末用 UML 圖來(lái)表示就是下面這樣漫畫(huà):AOP 面試造火箭事件始末Client 是直接和 Proxy 打交道的,Proxy 是 Client 要真正調(diào)用的 RealSubject 的代理,它確實(shí)執(zhí)行了 RealSubject 的 request 方法,不過(guò)在這個(gè)執(zhí)行前后 Proxy 也加上了額外的 PreRequest(),afterRequest() 方法,注意 Proxy 和 RealSubject 都實(shí)現(xiàn)了 Subject 這個(gè)接口,這樣在 Client 看起來(lái)調(diào)用誰(shuí)是沒(méi)有什么分別的(面向接口編程,對(duì)調(diào)用方無(wú)感,因?yàn)閷?shí)現(xiàn)的接口方法是一樣的),Proxy 通過(guò)其屬性持有真正要代理的目標(biāo)對(duì)象(RealSubject)以達(dá)到既能調(diào)用目標(biāo)對(duì)象的方法也能在方法前后注入其它邏輯的目的

漫畫(huà):AOP 面試造火箭事件始末

聽(tīng)得我要睡著了,根據(jù)這個(gè) UML 來(lái)寫(xiě)下相應(yīng)的實(shí)現(xiàn)類(lèi)吧

漫畫(huà):AOP 面試造火箭事件始末

沒(méi)問(wèn)題,不過(guò)在此之前我要先介紹一下代理的類(lèi)型,代理主要分為兩種類(lèi)型:靜態(tài)代理和動(dòng)態(tài)代理,動(dòng)態(tài)代理又有 JDK 代理和 CGLib 代理兩種,我先解釋下靜態(tài)和動(dòng)態(tài)的含義

漫畫(huà):AOP 面試造火箭事件始末

好小子,邏輯清晰,繼續(xù)吧

漫畫(huà):AOP 面試造火箭事件始末

要理解靜態(tài)和動(dòng)態(tài)這兩個(gè)含義,我們首先需要理解一下 Java 程序的運(yùn)行機(jī)制漫畫(huà):AOP 面試造火箭事件始末首先 Java 源代碼經(jīng)過(guò)編譯生成字節(jié)碼,然后再由 JVM 經(jīng)過(guò)類(lèi)加載,連接,初始化成 Java 類(lèi)型,可以看到字節(jié)碼是關(guān)鍵,靜態(tài)和動(dòng)態(tài)的區(qū)別就在于字節(jié)碼生成的時(shí)機(jī)靜態(tài)代理:由程序員創(chuàng)建代理類(lèi)或特定工具自動(dòng)生成源代碼再對(duì)其編譯。在編譯時(shí)已經(jīng)將接口,被代理類(lèi)(委托類(lèi)),代理類(lèi)等確定下來(lái),在程序運(yùn)行前代理類(lèi)的.class文件就已經(jīng)存在了動(dòng)態(tài)代理:在程序運(yùn)行后通過(guò)反射創(chuàng)建生成字節(jié)碼再由 JVM 加載而成

漫畫(huà):AOP 面試造火箭事件始末

好,那你寫(xiě)下靜態(tài)代理吧

漫畫(huà):AOP 面試造火箭事件始末

嘿嘿按這張 UML 類(lèi)庫(kù)依葫蘆畫(huà)瓢,傻瓜也會(huì)漫畫(huà):AOP 面試造火箭事件始末

public interface Subject { public void request();
} public class RealSubject implements Subject { @Override public void request() { // 賣(mài)房 System.out.println("賣(mài)房");
   }
} public class Proxy implements Subject { private RealSubject realSubject; public Proxy(RealSubject subject) { this.realSubject = subject;
   } @Override public void request() { // 執(zhí)行代理邏輯 System.out.println("賣(mài)房前"); // 執(zhí)行目標(biāo)對(duì)象方法 realSubject.request(); // 執(zhí)行代理邏輯 System.out.println("賣(mài)房后");
   } public static void main(String[] args) { // 被代理對(duì)象 RealSubject subject = new RealSubject(); // 代理 Proxy proxy = new Proxy(subject); // 代理請(qǐng)求 proxy.request();
   }
}
漫畫(huà):AOP 面試造火箭事件始末

喲喲喲,"傻瓜也會(huì)",看把你能的,那你說(shuō)下靜態(tài)代理有啥劣勢(shì)

漫畫(huà):AOP 面試造火箭事件始末

靜態(tài)代理主要有兩大劣勢(shì)

  1. 代理類(lèi)只代理一個(gè)委托類(lèi)(其實(shí)可以代理多個(gè),但不符合單一職責(zé)原則),也就意味著如果要代理多個(gè)委托類(lèi),就要寫(xiě)多個(gè)代理(別忘了靜態(tài)代理在編譯前必須確定)
  2. 第一點(diǎn)還不是致命的,再考慮這樣一種場(chǎng)景:如果每個(gè)委托類(lèi)的每個(gè)方法都要被織入同樣的邏輯,比如說(shuō)我要計(jì)算前文提到的每個(gè)委托類(lèi)每個(gè)方法的耗時(shí),就要在方法開(kāi)始前,開(kāi)始后分別織入計(jì)算時(shí)間的代碼,那就算用代理類(lèi),它的方法也有無(wú)數(shù)這種重復(fù)的計(jì)算時(shí)間的代碼
漫畫(huà):AOP 面試造火箭事件始末

回答的不錯(cuò),那該怎么改進(jìn)

漫畫(huà):AOP 面試造火箭事件始末

嘿嘿,這就要提到動(dòng)態(tài)代理了,靜態(tài)代理的這些劣勢(shì)主要是是因?yàn)樵诰幾g前這些代理類(lèi)是確定的,如果這些代理類(lèi)是動(dòng)態(tài)生成的呢,是不是可以省略一大堆代理的代碼。

漫畫(huà):AOP 面試造火箭事件始末

給你 5 分鐘你先寫(xiě)一下 JDK 的動(dòng)態(tài)代理并解釋其原理

漫畫(huà):AOP 面試造火箭事件始末

動(dòng)態(tài)代理分為 JDK 提供的動(dòng)態(tài)代理和 Spring AOP 用到的 CGLib 生成的代理,我們先看下 JDK 提供的動(dòng)態(tài)代理該怎么寫(xiě)

漫畫(huà):AOP 面試造火箭事件始末

這是代碼

// 委托類(lèi) public class RealSubject implements Subject { @Override public void request() { // 賣(mài)房 System.out.println("賣(mài)房");
   }
} import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; public class ProxyFactory { private Object target;// 維護(hù)一個(gè)目標(biāo)對(duì)象 public ProxyFactory(Object target) { this.target = target;
   } // 為目標(biāo)對(duì)象生成代理對(duì)象 public Object getProxyInstance() { return Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                       System.out.println("計(jì)算開(kāi)始時(shí)間"); // 執(zhí)行目標(biāo)對(duì)象方法 method.invoke(target, args);
                       System.out.println("計(jì)算結(jié)束時(shí)間"); return null;
                   }
               });
   } public static void main(String[] args) {
       RealSubject realSubject = new RealSubject();
       System.out.println(realSubject.getClass());
       Subject subject = (Subject) new ProxyFactory(realSubject).getProxyInstance();
       System.out.println(subject.getClass());
       subject.request();
   }
}```
打印結(jié)果如下:
```shell
原始類(lèi):class com.example.demo.proxy.staticproxy.RealSubject 代理類(lèi):class com.sun.proxy.$Proxy0 計(jì)算開(kāi)始時(shí)間
賣(mài)房
計(jì)算結(jié)束時(shí)間 

我們注意到代理類(lèi)的 class 為 com.sun.proxy.$Proxy0,它是如何生成的呢,注意到 Proxy 是在 java.lang.reflect 反射包下的,注意看看 Proxy 的 newProxyInstance 簽名

public static Object newProxyInstance(ClassLoader loader,
                                         Class[] interfaces,
                                         InvocationHandler h);
  1. loader: 代理類(lèi)的ClassLoader,最終讀取動(dòng)態(tài)生成的字節(jié)碼,并轉(zhuǎn)成 java.lang.Class 類(lèi)的一個(gè)實(shí)例(即類(lèi)),通過(guò)此實(shí)例的 newInstance() 方法就可以創(chuàng)建出代理的對(duì)象
  2. interfaces: 委托類(lèi)實(shí)現(xiàn)的接口,JDK 動(dòng)態(tài)代理要實(shí)現(xiàn)所有的委托類(lèi)的接口
  3. InvocationHandler: 委托對(duì)象所有接口方法調(diào)用都會(huì)轉(zhuǎn)發(fā)到 InvocationHandler.invoke(),在 invoke() 方法里我們可以加入任何需要增強(qiáng)的邏輯 主要是根據(jù)委托類(lèi)的接口等通過(guò)反射生成的
漫畫(huà):AOP 面試造火箭事件始末

這樣的實(shí)現(xiàn)有啥好處呢

漫畫(huà):AOP 面試造火箭事件始末

由于動(dòng)態(tài)代理是程序運(yùn)行后才生成的,哪個(gè)委托類(lèi)需要被代理到,只要生成動(dòng)態(tài)代理即可,避免了靜態(tài)代理那樣的硬編碼,另外所有委托類(lèi)實(shí)現(xiàn)接口的方法都會(huì)在 Proxy 的 InvocationHandler.invoke() 中執(zhí)行,這樣如果要統(tǒng)計(jì)所有方法執(zhí)行時(shí)間這樣相同的邏輯,可以統(tǒng)一在 InvocationHandler 里寫(xiě), 也就避免了靜態(tài)代理那樣需要在所有的方法中插入同樣代碼的問(wèn)題,代碼的可維護(hù)性極大的提高了。

漫畫(huà):AOP 面試造火箭事件始末

說(shuō)得這么厲害,那么 Spring AOP 的實(shí)現(xiàn)為啥卻不用它呢

漫畫(huà):AOP 面試造火箭事件始末

JDK 動(dòng)態(tài)代理雖好,但也有弱點(diǎn),我們注意到 newProxyInstance 的方法簽名

public static Object newProxyInstance(ClassLoader loader,
                                         Class[] interfaces,
                                         InvocationHandler h);

注意第二個(gè)參數(shù) Interfaces 是委托類(lèi)的接口,是必傳的, JDK 動(dòng)態(tài)代理是通過(guò)與委托類(lèi)實(shí)現(xiàn)同樣的接口,然后在實(shí)現(xiàn)的接口方法里進(jìn)行增強(qiáng)來(lái)實(shí)現(xiàn)的,這就意味著如果要用 JDK 代理,委托類(lèi)必須實(shí)現(xiàn)接口,這樣的實(shí)現(xiàn)方式看起來(lái)有點(diǎn)蠢,更好的方式是什么呢,直接繼承自委托類(lèi)不就行了,這樣委托類(lèi)的邏輯不需要做任何改動(dòng),CGlib 就是這么做的

漫畫(huà):AOP 面試造火箭事件始末

回答得不錯(cuò),接下來(lái)談?wù)?CGLib 動(dòng)態(tài)代理吧

漫畫(huà):AOP 面試造火箭事件始末

好嘞,開(kāi)頭我們提到的 AOP 就是用的 CGLib 的形式來(lái)生成的,JDK 動(dòng)態(tài)代理使用 Proxy 來(lái)創(chuàng)建代理類(lèi),增強(qiáng)邏輯寫(xiě)在 InvocationHandler.invoke() 里,CGlib 動(dòng)態(tài)代理也提供了類(lèi)似的  Enhance 類(lèi),增強(qiáng)邏輯寫(xiě)在 MethodInterceptor.intercept() 中,也就是說(shuō)所有委托類(lèi)的非 final 方法都會(huì)被方法攔截器攔截,在說(shuō)它的原理之前首先來(lái)看看它怎么用的

public class MyMethodInterceptor implements MethodInterceptor { @Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
       System.out.println("目標(biāo)類(lèi)增強(qiáng)前?。?!"); //注意這里的方法調(diào)用,不是用反射哦!??! Object object = proxy.invokeSuper(obj, args);
       System.out.println("目標(biāo)類(lèi)增強(qiáng)后!?。?); return object;
   }
} public class CGlibProxy { public static void main(String[] args) { //創(chuàng)建Enhancer對(duì)象,類(lèi)似于JDK動(dòng)態(tài)代理的Proxy類(lèi),下一步就是設(shè)置幾個(gè)參數(shù) Enhancer enhancer = new Enhancer(); //設(shè)置目標(biāo)類(lèi)的字節(jié)碼文件 enhancer.setSuperclass(RealSubject.class); //設(shè)置回調(diào)函數(shù) enhancer.setCallback(new MyMethodInterceptor()); //這里的creat方法就是正式創(chuàng)建代理類(lèi) RealSubject proxyDog = (RealSubject) enhancer.create(); //調(diào)用代理類(lèi)的eat方法 proxyDog.request();
   }
}

打印如下

代理類(lèi):class com.example.demo.proxy.staticproxy.RealSubject$$EnhancerByCGLIB$$889898c5
目標(biāo)類(lèi)增強(qiáng)前!?。?
賣(mài)房
目標(biāo)類(lèi)增強(qiáng)后!!!

可以看到主要就是利用 Enhancer 這個(gè)類(lèi)來(lái)設(shè)置委托類(lèi)與方法攔截器,這樣委托類(lèi)的所有非 final 方法就能被方法攔截器攔截,從而在攔截器里實(shí)現(xiàn)增強(qiáng)

漫畫(huà):AOP 面試造火箭事件始末

底層實(shí)現(xiàn)原理是啥

漫畫(huà):AOP 面試造火箭事件始末

之前也說(shuō)了它是通過(guò)繼承自委托類(lèi),重寫(xiě)委托類(lèi)的非 final 方法(final 方法不能重載),并在方法里調(diào)用委托類(lèi)的方法來(lái)實(shí)現(xiàn)代碼增強(qiáng)的,它的實(shí)現(xiàn)大概是這樣

public class RealSubject { @Override public void request() { // 賣(mài)房 System.out.println("賣(mài)房");
   }
} /** 生成的動(dòng)態(tài)代理類(lèi)(簡(jiǎn)化版)**/ public class RealSubject$$EnhancerByCGLIB$$889898c5 extends RealSubject { @Override public void request() {
       System.out.println("增強(qiáng)前"); super.request();
       System.out.println("增強(qiáng)后");
   }
}

可以看到它并不要求委托類(lèi)實(shí)現(xiàn)任何接口,而且 CGLIB 是高效的代碼生成包,底層依靠 ASM(開(kāi)源的 java 字節(jié)碼編輯類(lèi)庫(kù))操作字節(jié)碼實(shí)現(xiàn)的,性能比 JDK 強(qiáng),所以 Spring AOP 最終使用了 CGlib 來(lái)生成動(dòng)態(tài)代理

漫畫(huà):AOP 面試造火箭事件始末

CGlib 動(dòng)態(tài)代理使用上有啥限制嗎

漫畫(huà):AOP 面試造火箭事件始末

第一點(diǎn)之前已經(jīng)已經(jīng)說(shuō)了,只能代理委托類(lèi)中任意的非 final 的方法,另外它是通過(guò)繼承自委托類(lèi)來(lái)生成代理的,所以如果委托類(lèi)是 final 的,就無(wú)法被代理了(final 類(lèi)不能被繼承)

漫畫(huà):AOP 面試造火箭事件始末

小伙子,這次確實(shí)可以看出你作了非常充分的準(zhǔn)備,不過(guò)你答的這些網(wǎng)上都能搜到答案,為了防止一些候選人背書(shū)本,我這里還有最后一個(gè)問(wèn)題:JDK 動(dòng)態(tài)代理的攔截對(duì)象是通過(guò)反射的機(jī)制來(lái)調(diào)用被攔截方法的,CGlib 呢,它通過(guò)什么機(jī)制來(lái)提升了方法的調(diào)用效率。

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

嘿嘿,我猜到了你不知道,我告訴你吧,由于反射的效率比較低,所以 CGlib 采用了FastClass 的機(jī)制來(lái)實(shí)現(xiàn)對(duì)被攔截方法的調(diào)用。FastClass 機(jī)制就是對(duì)一個(gè)類(lèi)的方法建立索引,通過(guò)索引來(lái)直接調(diào)用相應(yīng)的方法,建議參考下https://www.cnblogs.com/cruze/p/3865180.html這個(gè)鏈接好好學(xué)學(xué)

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

還有一個(gè)問(wèn)題,我們通過(guò)打印類(lèi)名的方式知道了 cglib 生成了 RealSubject EnhancerByCGLIB$$889898c5 這樣的動(dòng)態(tài)代理,那么有反編譯過(guò)它的 class 文件來(lái)了解 cglib 代理類(lèi)的生成規(guī)則嗎

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

也在參考鏈接里,既然出來(lái)面試,對(duì)每個(gè)技術(shù)點(diǎn)都要深挖才行,像 Redis, MQ 這些中間件等平時(shí)只會(huì)用是不行的,對(duì)這些技術(shù)一定要做到原理級(jí)別的了解,鑒于你最后兩題沒(méi)答出來(lái),我認(rèn)為你造火箭能力還有待提高,先回去等通知吧

漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末 漫畫(huà):AOP 面試造火箭事件始末

后記

AOP 是 Spring 一個(gè)非常重要的特性,通過(guò)切面編程有效地實(shí)現(xiàn)了不同模塊相同行為的統(tǒng)一管理,也與業(yè)務(wù)邏輯實(shí)現(xiàn)了有效解藕,善用 AOP 有時(shí)候能起到出奇制勝的效果,舉一個(gè)例子,我們業(yè)務(wù)中有這樣的一個(gè)需求,需要在不同模塊中一些核心邏輯執(zhí)行前過(guò)一遍風(fēng)控,風(fēng)控通過(guò)了,這些核心邏輯才能執(zhí)行,怎么實(shí)現(xiàn)呢,你當(dāng)然可以統(tǒng)一封裝一個(gè)風(fēng)控工具類(lèi),然后在這些核心邏輯執(zhí)行前插入風(fēng)控工具類(lèi)的代碼,但這樣的話(huà)核心邏輯與非核心邏輯(風(fēng)控,事務(wù)等)就藕合在一起了,更好的方式顯然應(yīng)該用 AOP,使用文中所述的注解 + AOP 的方式,將這些非核心邏輯解藕到切面中執(zhí)行,讓代碼的可維護(hù)性大大提高了。

篇幅所限,文中沒(méi)有分析 JDK 和 CGlib 的動(dòng)態(tài)代理生成的實(shí)現(xiàn),不過(guò)建議大家有余力的話(huà)還是可以看看,尤其是文末的參考鏈接,生成動(dòng)態(tài)代理主要用到了反射的特性,不過(guò)我們知道反射存在一定的性能問(wèn)題,為了提升性能,底層用了一些比如緩存字節(jié)碼,F(xiàn)astClass 之類(lèi)的技術(shù)來(lái)提升性能,通讀源碼之后的,對(duì)反射的理解也會(huì)大大加深。



免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺(tái)僅提供信息存儲(chǔ)服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺(tái)立場(chǎng),如有問(wèn)題,請(qǐng)聯(lián)系我們,謝謝!


本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專(zhuān)欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車(chē)的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車(chē)技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車(chē)工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車(chē)。 SODA V工具的開(kāi)發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車(chē) 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶(hù)希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開(kāi)幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱(chēng),數(shù)字世界的話(huà)語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱(chēng)"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉