AOP面試造火箭始末

本文已整理致個人github地址,歡迎你們 star 支持一下html

這是一個困擾我司由來已久的難題,Dubbo 瞭解過吧,對外提供的服務可能有多個方法,通常咱們爲了避免給調用方埋坑,會在每一個方法裏把全部異常都 catch 住,只返回一個 result,調用方會根據這個 result 裏的 success 判斷這次調用是否成功,舉個例子java

public class ServiceResultTO<Textends Serializable {
   private static final long serialVersionUID = xxx;
   private Boolean success;
   private String message;
   private T data;
}

public interface TestService {
   ServiceResultTO<Boolean> test();
}

public class TestServiceImpl implements TestService {
   @Override
   public ServiceResultTO<Boolean> test() {
       try {
           // 此處寫服務裏的執行邏輯
           return ServiceResultTO.buildSuccess(Boolean.TRUE);
       } catch(Exception e) {
         return ServiceResultTO.buildFailed(Boolean.FALSE, "執行失敗");            
       }
   }
}

好比如今以上這樣的 dubbo 服務(TestService),它有一個 test 方法,爲了執行正常邏輯時出現異常,咱們在此方法執行邏輯外包了一層「try... catch...」若是隻有一個 test 方法,這樣作固然沒問題,但問題是在工程裏咱們通常要要提供幾十上百個 service,每一個 service 有幾十個像 test 這樣的方法,若是每一個方法都要在執行的時候包一層 「try ...catch...」,雖然可行,但代碼會比較醜陋,可讀性也比較差,你能想一想辦法改進一下嗎?git

既然是用切面解決的,我先解釋下什麼是切面。咱們知道,面向對象將程序抽象成多個層次的對象,每一個對象負責不一樣的模塊,這樣的話各個對象分工明確,各司其職,也不互相藕合,確實有力地促進了工程開發與分工協做,可是新的問題來了,不一樣的模塊(對象)間有時會出現公共的行爲,這種公共的行爲很難經過繼承的方式來實現,若是用工具類的話也不利於維護,代碼也顯得異常繁瑣。 切面(AOP)的引入就是爲了解決這類問題而生的,它要達到的效果是保證開發者在不修改源代碼的前提下,爲系統中不一樣的業務組件添加某些通用功能。程序員

舉個例子來講說github

好比上面這個例子,三個 service 對象執行過程當中都存在安全,事務,緩存,性能等相同行爲,這些相同的行爲顯然應該在同一個地方管理,有人說我能夠寫一個統一的工具類,在這些對象的方法前/後都嵌入此工具類,那問題來了,這些行爲都屬於業務無關的,使用工具類嵌入的方式致使與業務代碼緊藕合,很不合工程規範,代碼可維護性極差!切面就是爲了解決此類問題應運而生的,能作到相同功能的統一管理,對業務代碼無侵入web

以性能爲例,這些對象負責的模塊存在哪些類似的功能呢面試

好比說吧,每一個 service 都有不一樣的方法,我想統計每一個方法的執行時間,若是不用切面你須要在每一個方法的首尾計算下時間,而後相減正則表達式

若是我要統計每個 service 中每一個方法的執行時間可想而知不用切面的話就得在每一個方法的首尾都加上相似上述的邏輯,顯然這樣的代碼可維護性是很是差的,這還只是統計時間,若是此方法又要加上事務,風控等,是否是也得在方法首尾加上事務開始,回滾等代碼,可想而知業務代碼與非業務代碼嚴重藕合,這樣的實現方式對工程是一種災難,是不能接受的!shell

那若是用切面該怎麼作呢express

在說解決方案前,首先咱們要看下與切面相關的幾個定義 JoinPoint: 程序在執行流程中通過的一個個時間點,這個時間點能夠是方法調用時,或者是執行方法中異常拋出時,也能夠是屬性被修改時等時機,在這些時間點上你的切面代碼是能夠(注意是能夠但未必)被注入的

Pointcut: JoinPoints 只是切面代碼**能夠被織入(加強)**的地方,但我並不想對全部的 JoinPoint 進行織入,這就須要某些條件來篩選出那些須要被織入的 JoinPoint,Pointcut 就是經過一組規則(使用 AspectJ pointcut expression language 來描述) 來定位到匹配的 Joinpoint

Advice: 代碼織入(也叫加強),Pointcut 經過其規則指定了哪些 JoinPoint 能夠被織入,而 Advice 則指定了這些 Joinpoint 被織入(或者加強)的具體時機與邏輯,是切面代碼真正被執行的地方,主要有五個織入時機

  1. Before Advice: 在 JoinPoints 執行前織入
  2. After Advice: 在 JoinPoints 執行後織入(不論是否拋出異常都會織入)
  3. After returning advice: 在 JoinPoints 執行正常退出後織入(拋出異常則不會被織入)
  4. After throwing advice: 方法執行過程當中拋出異常後織入
  5. Around Advice: 這是全部 Advice 中最強大的,它在 JoinPoints 先後均可織入切面代碼,也能夠選擇是否執行原有正常的邏輯,若是不執行原有流程,它甚至能夠用本身的返回值代替原有的返回值,甚至拋出異常。 在這些 advice 裏咱們就能夠寫入切面代碼了 綜上所述,切面(Aspect)咱們能夠認爲就是 pointcut 和 advice,pointcut 指定了哪些 joinpoint 能夠被織入,而 advice 則指定了在這些 joinpoint 上的代碼織入時機與邏輯。

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

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

這也難不倒我,好比在餐館裏點菜,菜單有 10 個菜,這 10 個菜就是 JoinPoint,但我只點了帶有蘿蔔名字的菜,那麼帶有蘿蔔名字這個條件就是針對 JoinPoint(10 個菜)的篩選條件,即 pointcut,最終只有胡蘿蔔,白蘿蔔這兩個 JoinPoint 知足條件,而後咱們就能夠在吃胡蘿蔔前洗手(before advice),或吃胡蘿蔔後買單(after advice),也能夠統計吃胡蘿蔔的時間(around advice),這些洗手,買單,統計時間的動做都是與吃蘿蔔這個業務動做解藕的,都是統一寫在 advice 的邏輯裏

可否用程序實現一下,talk is cheap, show me your code!

好嘞,讓你看下個人實力

 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("吃白菜");
   }
}

假設有以上 TestService, 實現了吃蘿蔔,吃蘑菇,吃白菜三個方法,這三個方法均可以織入切面代碼,因此它們都是 JoinPoints,但如今我只想對吃蘿蔔這個 JoinPoints 先後織入 advice,首先固然要聲明 PointCut 表達式,這個表達式代表只想織入吃蘿蔔這個 JoinPoint,指明瞭以後再讓 advice 應用於此 pointcut 不就完了,好比我想在吃蘿蔔前洗手,吃蘿蔔後買單,能夠寫出以下切面邏輯

@Aspect
@Component
public class TestAdvice {
   // 1. 定義 PointCut
   @Pointcut("execution(* com.example.demo.api.TestServiceImpl.eatCarrot())")
   private void eatCarrot(){}

   // 2. 定義應用於 JoinPoint 中全部知足 PointCut 條件的 advice, 這裏咱們使用 around advice,在其中織入加強邏輯
   @Around("eatCarrot()")
   public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable {
       // 3. TestServiceImpl.eatCarrot 執行前邏輯
       System.out.println("吃蘿蔔前洗手");
       //  原來的 TestServiceImpl.eatCarrot 邏輯,可視狀況決定是否執行
       point.proceed();
       // 4. TestServiceImpl.eatCarrot 執行後邏輯
       System.out.println("吃蘿後買單");
   }
}

能夠看到經過 AOP 咱們巧妙地在方法執行先後執行插入相關的邏輯,對原有執行邏輯無任何侵入!

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

這就要說到 PointCut 的 AspectJ pointcut expression language 聲明式表達式,這個表達式支持的表達類型比較全面,能夠用正則,註解等來指定知足條件的 joinpoint , 好比類名後加 .*(..) 這樣的正則表達式就表明這個類裏面的全部方法都會被織入,使用 @annotation 的方式也能夠指定對標有這類註解的方法織入代碼

恩,能夠,繼續

首先咱們先定義一個以下註解

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

}

而後將全部 service 中方法裏的 「try... catch...」移除掉,在方法簽名上加上上述咱們定義好的註解

public class TestServiceImpl implements TestService {
   @Override
   @GlobalErrorCatch
   public ServiceResultTO<Boolean> test() {
        // 此處寫服務裏的執行邏輯
        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("執行錯誤" + e);
           return ServiceResultTO.buildFailed("系統錯誤");
       }
   }

}

經過這樣的方式,全部標記着 GlobalErrorCatch 註解的方法都會統一在 handlerGlobalResult 方法裏執行,咱們就能夠在這個方法裏統一 catch 住異常,全部 service 方法中又長又臭的 「try...catch...」所有幹掉,真香!

按照大佬提供的思路,我首先打印了 TestServiceImp 這個 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. 定義應用於 PointCut 的 advice, 這裏咱們使用 around advice
   @Around("eatCarrot()")
   public void handlerRpcResult(ProceedingJoinPoint point) throws Throwable {
        // 省略相關邏輯
   }
}
@SpringBootApplication
@EnableAspectJAutoProxy
public class DemoApplication {
   public static void main(String[] args) {
       ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.classargs);
       TestService testService = context.getBean(TestService.class);
       System.out.println("testService = " + testService.getClass());
   }
}

打印後我果真發現了端倪,這個 bean 的 class 竟然不是 TestServiceImpl!而是com.example.demo.impl.TestServiceImpl EnhancerBySpringCGLIB$$705c68c7!

果真有長進,繼續說,爲啥會生成這樣一個類

咱們注意到類名中有一個 EnhancerBySpringCGLIB ,注意 CGLiB,這個類就是經過它生成的動態代理

打住,先不要說動態代理,先談談啥是代理吧

代理在生活中隨處可見,好比說我要買房,我通常不會直接和賣家對接,通常會和中介打交道,中介就是代理,賣家就是目標對象,我就是調用者,代理不只實現了目標對象的行爲(幫目標對象賣房),還能夠添加上本身的動做(收保證金,籤合同等), 用 UML 圖來表示就是下面這樣 Client 是直接和 Proxy 打交道的,Proxy 是 Client 要真正調用的 RealSubject 的代理,它確實執行了 RealSubject 的 request 方法,不過在這個執行先後 Proxy 也加上了額外的 PreRequest(),afterRequest() 方法,注意 Proxy 和 RealSubject 都實現了 Subject 這個接口,這樣在 Client 看起來調用誰是沒有什麼分別的(面向接口編程,對調用方無感,由於實現的接口方法是同樣的),Proxy 經過其屬性持有真正要代理的目標對象(RealSubject)以達到既能調用目標對象的方法也能在方法先後注入其它邏輯的目的

聽得我要睡着了,根據這個 UML 來寫下相應的實現類吧

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

好小子,邏輯清晰,繼續吧

要理解靜態和動態這兩個含義,咱們首先須要理解一下 Java 程序的運行機制 首先 Java 源代碼通過編譯生成字節碼,而後再由 JVM 通過類加載,鏈接,初始化成 Java 類型,能夠看到字節碼是關鍵,靜態和動態的區別就在於字節碼生成的時機 靜態代理: 由程序員建立代理類或特定工具自動生成源代碼再對其編譯。在編譯時已經將接口,被代理類(委託類),代理類等肯定下來,在程序運行前代理類的.class文件就已經存在了 動態代理:在程序運行後經過反射建立生成字節碼再由 JVM 加載而成

好,那你寫下靜態代理吧

嘿嘿按這張 UML 類庫依葫蘆畫瓢,傻瓜也會

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() {
      // 執行代理邏輯
       System.out.println("賣房前");

       // 執行目標對象方法
       realSubject.request();

       // 執行代理邏輯
       System.out.println("賣房後");
   }

   public static void main(String[] args) {
       // 被代理對象
       RealSubject subject = new RealSubject();

       // 代理
       Proxy proxy = new Proxy(subject);

       // 代理請求
       proxy.request();
   }
}

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

靜態代理主要有兩大劣勢

  1. 代理類只代理一個委託類(其實能夠代理多個,但不符合單一職責原則),也就意味着若是要代理多個委託類,就要寫多個代理(別忘了靜態代理在編譯前必須肯定)
  2. 第一點還不是致命的,再考慮這樣一種場景:若是每一個委託類的每一個方法都要被織入一樣的邏輯,好比說我要計算前文提到的每一個委託類每一個方法的耗時,就要在方法開始前,開始後分別織入計算時間的代碼,那就算用代理類,它的方法也有無數這種重複的計算時間的代碼

回答的不錯,那該怎麼改進

嘿嘿,這就要提到動態代理了,靜態代理的這些劣勢主要是是由於在編譯前這些代理類是肯定的,若是這些代理類是動態生成的呢,是否是能夠省略一大堆代理的代碼。

給你 5 分鐘你先寫一下 JDK 的動態代理並解釋其原理

動態代理分爲 JDK 提供的動態代理和 Spring AOP 用到的 CGLib 生成的代理,咱們先看下 JDK 提供的動態代理該怎麼寫

這是代碼

// 委託類
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;// 維護一個目標對象

   public ProxyFactory(Object target) {
       this.target = target;
   }

   // 爲目標對象生成代理對象
   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("計算開始時間");
                       // 執行目標對象方法
                       method.invoke(target, args);
                       System.out.println("計算結束時間");
                       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();
   }
}```
打印結果以下:
```shell
原始類:class com.example.demo.proxy.staticproxy.RealSubject
代理類:class com.sun.proxy.$Proxy0
計算開始時間
賣房
計算結束時間

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

public static Object newProxyInstance(ClassLoader loader,
                                         Class<?>[] interfaces,
                                         InvocationHandler h)
;
  1. loader: 代理類的ClassLoader,最終讀取動態生成的字節碼,並轉成 java.lang.Class 類的一個實例(即類),經過此實例的 newInstance() 方法就能夠建立出代理的對象
  2. interfaces: 委託類實現的接口,JDK 動態代理要實現全部的委託類的接口
  3. InvocationHandler: 委託對象全部接口方法調用都會轉發到 InvocationHandler.invoke(),在 invoke() 方法裏咱們能夠加入任何須要加強的邏輯 主要是根據委託類的接口等經過反射生成的

這樣的實現有啥好處呢

因爲動態代理是程序運行後才生成的,哪一個委託類須要被代理到,只要生成動態代理便可,避免了靜態代理那樣的硬編碼,另外全部委託類實現接口的方法都會在 Proxy 的 InvocationHandler.invoke() 中執行,這樣若是要統計全部方法執行時間這樣相同的邏輯,能夠統一在 InvocationHandler 裏寫, 也就避免了靜態代理那樣須要在全部的方法中插入一樣代碼的問題,代碼的可維護性極大的提升了。

說得這麼厲害,那麼 Spring AOP 的實現爲啥卻不用它呢

JDK 動態代理雖好,但也有弱點,咱們注意到 newProxyInstance 的方法簽名

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

注意第二個參數 Interfaces 是委託類的接口,是必傳的, JDK 動態代理是經過與委託類實現一樣的接口,而後在實現的接口方法裏進行加強來實現的,這就意味着若是要用 JDK 代理,委託類必須實現接口,這樣的實現方式看起來有點蠢,更好的方式是什麼呢,直接繼承自委託類不就好了,這樣委託類的邏輯不須要作任何改動,CGlib 就是這麼作的

回答得不錯,接下來談談 CGLib 動態代理吧

好嘞,開頭咱們提到的 AOP 就是用的 CGLib 的形式來生成的,JDK 動態代理使用 Proxy 來建立代理類,加強邏輯寫在 InvocationHandler.invoke() 裏,CGlib 動態代理也提供了相似的 Enhance 類,加強邏輯寫在 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("目標類加強前!!!");
       //注意這裏的方法調用,不是用反射哦!!!
       Object object = proxy.invokeSuper(obj, args);
       System.out.println("目標類加強後!!!");
       return object;
   }
}

public class CGlibProxy {
   public static void main(String[] args) {
       //建立Enhancer對象,相似於JDK動態代理的Proxy類,下一步就是設置幾個參數
       Enhancer enhancer = new Enhancer();
       //設置目標類的字節碼文件
       enhancer.setSuperclass(RealSubject.class);
       //設置回調函數
       enhancer.setCallback(new MyMethodInterceptor());

       //這裏的creat方法就是正式建立代理類
       RealSubject proxyDog = (RealSubject) enhancer.create();
       //調用代理類的eat方法
       proxyDog.request();
   }
}

打印以下

代理類:class com.example.demo.proxy.staticproxy.RealSubject$$EnhancerByCGLIB$$889898c5
目標類加強前!!!
賣房
目標類加強後!!!

能夠看到主要就是利用 Enhancer 這個類來設置委託類與方法攔截器,這樣委託類的全部非 final 方法就能被方法攔截器攔截,從而在攔截器裏實現加強

底層實現原理是啥

以前也說了它是經過繼承自委託類,重寫委託類的非 final 方法(final 方法不能重載),並在方法裏調用委託類的方法來實現代碼加強的,它的實現大概是這樣

public class RealSubject {
   @Override
   public void request() {
       // 賣房
       System.out.println("賣房");
   }
}

/** 生成的動態代理類(簡化版)**/
public class RealSubject$$EnhancerByCGLIB$$889898c5 extends RealSubject {
   @Override
   public void request() {
       System.out.println("加強前");
       super.request();
       System.out.println("加強後");
   }
}

能夠看到它並不要求委託類實現任何接口,並且 CGLIB 是高效的代碼生成包,底層依靠 ASM(開源的 java 字節碼編輯類庫)操做字節碼實現的,性能比 JDK 強,因此 Spring AOP 最終使用了 CGlib 來生成動態代理

CGlib 動態代理使用上有啥限制嗎

第一點以前已經已經說了,只能代理委託類中任意的非 final 的方法,另外它是經過繼承自委託類來生成代理的,因此若是委託類是 final 的,就沒法被代理了(final 類不能被繼承)

小夥子,此次確實能夠看出你做了很是充分的準備,不過你答的這些網上都能搜到答案,爲了防止一些候選人背書本,我這裏還有最後一個問題: JDK 動態代理的攔截對象是經過反射的機制來調用被攔截方法的,CGlib 呢,它經過什麼機制來提高了方法的調用效率。

嘿嘿,我猜到了你不知道,我告訴你吧,因爲反射的效率比較低,因此 CGlib 採用了FastClass 的機制來實現對被攔截方法的調用。FastClass 機制就是對一個類的方法創建索引,經過索引來直接調用相應的方法,建議參考下https://www.cnblogs.com/cruze/p/3865180.html這個連接好好學學

還有一個問題,咱們經過打印類名的方式知道了 cglib 生成了 RealSubject EnhancerByCGLIB$$889898c5 這樣的動態代理,那麼有反編譯過它的 class 文件來了解 cglib 代理類的生成規則嗎

也在參考連接裏,既然出來面試,對每一個技術點都要深挖才行,像 Redis, MQ 這些中間件等平時只會用是不行的,對這些技術必定要作到原理級別的瞭解,鑑於你最後兩題沒答出來,我認爲你造火箭能力還有待提升,先回去等通知吧

後記

AOP 是 Spring 一個很是重要的特性,經過切面編程有效地實現了不一樣模塊相同行爲的統一管理,也與業務邏輯實現了有效解藕,善用 AOP 有時候能起到出奇制勝的效果,舉一個例子,咱們業務中有這樣的一個需求,須要在不一樣模塊中一些核心邏輯執行前過一遍風控,風控經過了,這些核心邏輯才能執行,怎麼實現呢,你固然能夠統一封裝一個風控工具類,而後在這些核心邏輯執行前插入風控工具類的代碼,但這樣的話核心邏輯與非核心邏輯(風控,事務等)就藕合在一塊兒了,更好的方式顯然應該用 AOP,使用文中所述的註解 + AOP 的方式,將這些非核心邏輯解藕到切面中執行,讓代碼的可維護性大大提升了。

篇幅所限,文中沒有分析 JDK 和 CGlib 的動態代理生成的實現,不過建議你們有餘力的話仍是能夠看看,尤爲是文末的參考連接,生成動態代理主要用到了反射的特性,不過咱們知道反射存在必定的性能問題,爲了提高性能,底層用了一些好比緩存字碼碼,FastClass 之類的技術來提高性能,通讀源碼以後的,對反射的理解也會大大加深。

巨人的肩膀

  • Spring AOP 是怎麼運行的?完全搞定這道面試必考題 https://cloud.tencent.com/developer/article/1584491

最後歡迎你們關注個人公號,加我好友:「geekoftaste」,一塊兒交流,共同進步!

相關文章
相關標籤/搜索