漫畫(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)一下嗎?
既然是用切面解決的,我先解釋下什么是切面。我們知道,面向?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ù)組件添加某些通用功能。
舉個(gè)例子來(lái)說(shuō)說(shuō)
比如上面這個(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ú)侵入
以性能為例,這些對(duì)象負(fù)責(zé)的模塊存在哪些相似的功能呢
比如說(shuō)吧,每個(gè) service 都有不同的方法,我想統(tǒng)計(jì)每個(gè)方法的執(zhí)行時(shí)間,如果不用切面你需要在每個(gè)方法的首尾計(jì)算下時(shí)間,然后相減
如果我要統(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)難,是不能接受的!
那如果用切面該怎么做呢
在說(shuō)解決方案前,首先我們要看下與切面相關(guān)的幾個(gè)定義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ī)
畫(huà)外音:織入(weaving),將切面作用于委托類(lèi)對(duì)象以創(chuàng)建 adviced object 的過(guò)程(即代理,下文會(huì)提)
- Before Advice: 在 JoinPoints 執(zhí)行前織入
- After Advice: 在 JoinPoints 執(zhí)行后織入(不管是否拋出異常都會(huì)織入)
- After returning advice: 在 JoinPoints 執(zhí)行正常退出后織入(拋出異常則不會(huì)被織入)
- After throwing advice: 方法執(zhí)行過(guò)程中拋出異常后織入
- Around Advice: 這是所有 Advice 中最強(qiáng)大的,它在 JoinPoints 前后都可織入切面代碼,也可以選擇是否執(zhí)行原有正常的邏輯,如果不執(zhí)行原有流程,它甚至可以用自己的返回值代替原有的返回值,甚至拋出異常。在這些 advice 里我們就可以寫(xiě)入切面代碼了 綜上所述,切面(Aspect)我們可以認(rèn)為就是 pointcut 和 advice,pointcut 指定了哪些 joinpoint 可以被織入,而 advice 則指定了在這些 joinpoint 上的代碼織入時(shí)機(jī)與邏輯
列了一大堆概念真讓人生氣,請(qǐng)用你奶奶都能聽(tīng)得懂的語(yǔ)言來(lái)解釋一下這些概念!
把技術(shù)解釋得讓非技術(shù)的人也聽(tīng)懂才叫本事,這才說(shuō)明你真的懂了。
這也難不倒我,比如在餐館里點(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 的邏輯里
能否用程序?qū)崿F(xiàn)一下,talk is cheap, show me your code!
好嘞,讓你看下我的實(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ú)任何侵入!
小子果然有兩把刷子,我們 HR 眼光不錯(cuò),還有一個(gè)問(wèn)題,開(kāi)頭我司的那個(gè)難題你用切面又是如何解決的呢。
這就要說(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)注解的方法織入代碼
恩,可以,繼續(xù)
首先我們先定義一個(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...」全部干掉,真香!
按照大佬提供的思路,我首先打印了 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()); } }打印后我果然發(fā)現(xiàn)了端倪,這個(gè) bean 的 class 居然不是 TestServiceImpl!而是com.example.demo.impl.TestServiceImpl EnhancerBySpringCGLIB$$705c68c7!
果然有長(zhǎng)進(jìn),繼續(xù)說(shuō),為啥會(huì)生成這樣一個(gè)類(lèi)
我們注意到類(lèi)名中有一個(gè) EnhancerBySpringCGLIB ,注意 CGLiB,這個(gè)類(lèi)就是通過(guò)它生成的動(dòng)態(tài)代理
打住,先不要說(shuō)動(dòng)態(tài)代理,先談?wù)勆妒谴戆?
代理在生活中隨處可見(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)作(收保證金,簽合同等),用 UML 圖來(lái)表示就是下面這樣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ì)象的方法也能在方法前后注入其它邏輯的目的
聽(tīng)得我要睡著了,根據(jù)這個(gè) UML 來(lái)寫(xiě)下相應(yīng)的實(shí)現(xiàn)類(lèi)吧
沒(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)的含義
好小子,邏輯清晰,繼續(xù)吧
要理解靜態(tài)和動(dòng)態(tài)這兩個(gè)含義,我們首先需要理解一下 Java 程序的運(yùn)行機(jī)制首先 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 加載而成
好,那你寫(xiě)下靜態(tài)代理吧
嘿嘿按這張 UML 類(lèi)庫(kù)依葫蘆畫(huà)瓢,傻瓜也會(huì)
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ì)",看把你能的,那你說(shuō)下靜態(tài)代理有啥劣勢(shì)
靜態(tài)代理主要有兩大劣勢(shì)
- 代理類(lèi)只代理一個(gè)委托類(lèi)(其實(shí)可以代理多個(gè),但不符合單一職責(zé)原則),也就意味著如果要代理多個(gè)委托類(lèi),就要寫(xiě)多個(gè)代理(別忘了靜態(tài)代理在編譯前必須確定)
- 第一點(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í)間的代碼
回答的不錯(cuò),那該怎么改進(jìn)
嘿嘿,這就要提到動(dòng)態(tài)代理了,靜態(tài)代理的這些劣勢(shì)主要是是因?yàn)樵诰幾g前這些代理類(lèi)是確定的,如果這些代理類(lèi)是動(dòng)態(tài)生成的呢,是不是可以省略一大堆代理的代碼。
給你 5 分鐘你先寫(xiě)一下 JDK 的動(dòng)態(tài)代理并解釋其原理
動(dòng)態(tài)代理分為 JDK 提供的動(dòng)態(tài)代理和 Spring AOP 用到的 CGLib 生成的代理,我們先看下 JDK 提供的動(dòng)態(tài)代理該怎么寫(xiě)
這是代碼
// 委托類(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);
- 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ì)象
- interfaces: 委托類(lèi)實(shí)現(xiàn)的接口,JDK 動(dòng)態(tài)代理要實(shí)現(xiàn)所有的委托類(lèi)的接口
- InvocationHandler: 委托對(duì)象所有接口方法調(diào)用都會(huì)轉(zhuǎn)發(fā)到 InvocationHandler.invoke(),在 invoke() 方法里我們可以加入任何需要增強(qiáng)的邏輯 主要是根據(jù)委托類(lèi)的接口等通過(guò)反射生成的
這樣的實(shí)現(xiàn)有啥好處呢
由于動(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ù)性極大的提高了。
說(shuō)得這么厲害,那么 Spring AOP 的實(shí)現(xiàn)為啥卻不用它呢
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 就是這么做的
回答得不錯(cuò),接下來(lái)談?wù)?CGLib 動(dòng)態(tài)代理吧
好嘞,開(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)用,不是用反射哦!?。?/span> 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)
底層實(shí)現(xiàn)原理是啥
之前也說(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)代理
CGlib 動(dòng)態(tài)代理使用上有啥限制嗎
第一點(diǎn)之前已經(jīng)已經(jīng)說(shuō)了,只能代理委托類(lèi)中任意的非 final 的方法,另外它是通過(guò)繼承自委托類(lèi)來(lái)生成代理的,所以如果委托類(lèi)是 final 的,就無(wú)法被代理了(final 類(lèi)不能被繼承)
小伙子,這次確實(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)用效率。
嘿嘿,我猜到了你不知道,我告訴你吧,由于反射的效率比較低,所以 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é)
還有一個(gè)問(wèn)題,我們通過(guò)打印類(lèi)名的方式知道了 cglib 生成了 RealSubject EnhancerByCGLIB$$889898c5 這樣的動(dòng)態(tài)代理,那么有反編譯過(guò)它的 class 文件來(lái)了解 cglib 代理類(lèi)的生成規(guī)則嗎
也在參考鏈接里,既然出來(lái)面試,對(duì)每個(gè)技術(shù)點(diǎn)都要深挖才行,像 Redis, MQ 這些中間件等平時(shí)只會(huì)用是不行的,對(duì)這些技術(shù)一定要做到原理級(jí)別的了解,鑒于你最后兩題沒(méi)答出來(lái),我認(rèn)為你造火箭能力還有待提高,先回去等通知吧
后記
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ì)大大加深。