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


漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

這是一個(gè)困擾我司由來已久的難題,Dubbo 了解過吧,對外提供的服務(wù)可能有多個(gè)方法,一般我們?yōu)榱瞬唤o調(diào)用方埋坑,會在每個(gè)方法里把所有異常都 catch 住,只返回一個(gè) result,調(diào)用方會根據(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 { // 此處寫服務(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)異常,我們在此方法執(zhí)行邏輯外包了一層「try... catch...」如果只有一個(gè) test 方法,這樣做當(dāng)然沒問題,但問題是在工程里我們一般要要提供幾十上百個(gè) service,每個(gè) service 有幾十個(gè)像 test 這樣的方法,如果每個(gè)方法都要在執(zhí)行的時(shí)候包一層 「try ...catch...」,雖然可行,但代碼會比較丑陋,可讀性也比較差,你能想想辦法改進(jìn)一下嗎?

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

舉個(gè)例子來說說

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

那如果用切面該怎么做呢

漫畫:AOP 面試造火箭事件始末

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

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

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

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

列了一大堆概念真讓人生氣,請用你奶奶都能聽得懂的語言來解釋一下這些概念!

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫: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)在我只想對吃蘿卜這個(gè) joinpoints 前后織入 advice,該怎么辦呢,首先當(dāng)然要聲明 pointcut 表達(dá)式,這個(gè)表達(dá)式表明只想織入吃蘿卜這個(gè) joinpoint,指明了之后再讓 advice 應(yīng)用于此 pointcut 不就完了,比如我想在吃蘿卜前洗手,吃蘿卜后買單,可以寫出如下切面邏輯

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

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

恩,可以,繼續(xù)

漫畫:AOP 面試造火箭事件始末

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

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

}

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

public class TestServiceImpl implements TestService { @Override @GlobalErrorCatch public ServiceResultTOtest() { // 此處寫服務(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ò)誤");
       }
   }

}

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

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

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

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

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

漫畫:AOP 面試造火箭事件始末

果然有長進(jìn),繼續(xù)說,為啥會生成這樣一個(gè)類

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

嘿嘿按這張 UML 類庫依葫蘆畫瓢,傻瓜也會漫畫:AOP 面試造火箭事件始末

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

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

漫畫:AOP 面試造火箭事件始末

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

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

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

這是代碼

// 委托類 public class RealSubject implements Subject { @Override public void request() { // 賣房 System.out.println("賣房");
   }
} 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)對象 public ProxyFactory(Object target) { this.target = target;
   } // 為目標(biāo)對象生成代理對象 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ì)算開始時(shí)間"); // 執(zhí)行目標(biāo)對象方法 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
原始類:class com.example.demo.proxy.staticproxy.RealSubject 代理類:class com.sun.proxy.$Proxy0 計(jì)算開始時(shí)間
賣房
計(jì)算結(jié)束時(shí)間 

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

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

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

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

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

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

打印如下

代理類:class com.example.demo.proxy.staticproxy.RealSubject$$EnhancerByCGLIB$$889898c5
目標(biāo)類增強(qiáng)前?。。?
賣房
目標(biāo)類增強(qiáng)后?。。?/pre>

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

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

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

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

漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末 漫畫:AOP 面試造火箭事件始末

后記

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

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



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


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

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

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

加利福尼亞州圣克拉拉縣2024年8月30日 /美通社/ -- 數(shù)字化轉(zhuǎn)型技術(shù)解決方案公司Trianz今天宣布,該公司與Amazon Web Services (AWS)簽訂了...

關(guān)鍵字: AWS AN BSP 數(shù)字化

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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