面試別再問我什么是 Spring AOP 和代理了!

我們知道,Spring 中 AOP 是一大核心技術(shù),也是面試中經(jīng)常會(huì)被問到的問題,最近我在網(wǎng)上也看到很多面試題,其中和 Spring AOP 相關(guān)的就有不少,這篇文章主要來總結(jié)下相關(guān)的技術(shù)點(diǎn),希望對(duì)大家有用。

  1. 幾個(gè)常見的問題

針對(duì)這一塊的東西,一般下面幾個(gè)問題面試官問的比較多:
Spring AOP用的是哪種設(shè)計(jì)模式?
談?wù)勀銓?duì)代理模式的理解?
靜態(tài)代理和動(dòng)態(tài)代理有什么區(qū)別?
如何實(shí)現(xiàn)動(dòng)態(tài)代理?
Spring AOP中用的是哪種代理技術(shù)?

如果這些問題都能回答的很流暢的話,說明對(duì)代理這一塊的基本知識(shí)有一定的了解了。因?yàn)槲覀冊(cè)趯?shí)際開發(fā)中,寫業(yè)務(wù)代碼會(huì)更多,所以這一塊的東西,大部分人可能知道個(gè)一二,但是如果讓他們很有條理的表達(dá)出來,可能就不那么容易了。

  1. 什么是 Spring AOP?

一般面試官問到這個(gè)問題,面試者基本上都會(huì)回答:AOP 就是面向切面編程。其實(shí)這真的是句廢話,這么回答真的沒有任何意義。

或許你可以給面試官舉個(gè)例子:歌星都有好多助理,歌星最重要的一件事就是唱歌,其他事他不用關(guān)注,比如唱歌前可能需要和其他人談合作,還要布置場(chǎng)地,唱歌后還要收錢等等,這些統(tǒng)統(tǒng)交給他對(duì)應(yīng)的助理去做。也許哪一天,這個(gè)歌星做慈善,免費(fèi)唱歌了,不收錢了,那么就可以把收錢這個(gè)助力給辭退了。這就是 AOP,每個(gè)人各司其職,靈活組合,達(dá)到一種可配置的、可插拔的程序結(jié)構(gòu)。AOP 的實(shí)現(xiàn)原理就是代理模式。

在程序中也是如此,通過代理,可以詳細(xì)控制訪問某個(gè)或者某類對(duì)象的方法,在調(diào)用這個(gè)方法前做前置處理,調(diào)用這個(gè)方法后做后置處理。

  1. 什么是代理模式?

代理模式的核心作用就是通過代理,控制對(duì)對(duì)象的訪問。它的設(shè)計(jì)思路是:定義一個(gè)抽象角色,讓代理角色和真實(shí)角色分別去實(shí)現(xiàn)它。

真實(shí)角色:實(shí)現(xiàn)抽象角色,定義真實(shí)角色所要實(shí)現(xiàn)的業(yè)務(wù)邏輯,供代理角色調(diào)用。它只關(guān)注真正的業(yè)務(wù)邏輯,比如歌星唱歌。

代理角色:實(shí)現(xiàn)抽象角色,是真實(shí)角色的代理,通過真實(shí)角色的業(yè)務(wù)邏輯方法來實(shí)現(xiàn)抽象方法,并在前后可以附加自己的操作,比如談合同,布置場(chǎng)地,收錢等等。

這就是代理模式的設(shè)計(jì)思路。代理模式分為靜態(tài)代理和動(dòng)態(tài)代理。靜態(tài)代理是我們自己創(chuàng)建一個(gè)代理類,而動(dòng)態(tài)代理是程序自動(dòng)幫我們生成一個(gè)代理,我們就不用管了。下面我詳細(xì)介紹一下這兩種代理模式。

  1. 靜態(tài)代理模式

就舉明星唱歌這個(gè)例子,根據(jù)上面提供的設(shè)計(jì)思路,首先我們需要?jiǎng)?chuàng)建明星這個(gè)抽象角色,

/**

  • 明星接口類

  • @author shengwu ni

  • @date 2018-12-07

*/

public interface Star {

/**

* 唱歌方法

*/

void sing();

}

靜態(tài)代理需要?jiǎng)?chuàng)建真實(shí)角色和代理角色,分別實(shí)現(xiàn)唱歌這個(gè)接口,真實(shí)角色很簡單,直接實(shí)現(xiàn)即可,因?yàn)檎鎸?shí)角色的主要任務(wù)就是唱歌。

/**

  • 真實(shí)明星類

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class RealStar implements Star {

    @Override
    public void sing() {
    System.out.println("明星本人開始唱歌……");
    }
    }

代理類就需要做點(diǎn)工作了,我們思考一下,代理只是在明星唱歌前后做一些準(zhǔn)備和收尾的事,唱歌這件事還得明星親自上陣,代理做不了。所以代理類里面是肯定要將真實(shí)的對(duì)象傳進(jìn)來。有了思路,我們將代理類寫出來。

/**

  • 明星的靜態(tài)代理類

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class ProxyStar implements Star {

    /**

    • 接收真實(shí)的明星對(duì)象
      */
      private Star star;

    /**

    • 通過構(gòu)造方法傳進(jìn)來真實(shí)的明星對(duì)象
    • @param star star
      */
      public ProxyStar(Star star) {
      this.star = star;
      }

    @Override
    public void sing() {
    System.out.println("代理先進(jìn)行談判……");
    // 唱歌只能明星自己唱
    this.star.sing();
    System.out.println("演出完代理去收錢……");
    }

}

這樣的話,邏輯就非常清晰了。在代理類中,可以看到,維護(hù)了一個(gè)Star對(duì)象,通過構(gòu)造方法傳進(jìn)來一個(gè)真實(shí)的Star對(duì)象給其賦值,然后在唱歌這個(gè)方法里,使用真實(shí)對(duì)象來唱歌。所以說面談、收錢都是由代理對(duì)象來實(shí)現(xiàn)的,唱歌是代理對(duì)象讓真實(shí)對(duì)象來做。下面寫個(gè)客戶端測(cè)試下。

/**

  • 測(cè)試客戶端

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class Client {

    /**

    • 測(cè)試靜態(tài)代理結(jié)果

    • @param args args
      */
      public static void main(String[] args) {
      Star realStar = new RealStar();
      Star proxy = new ProxyStar(realStar);

      proxy.sing();
      }
      }

讀者可以自己運(yùn)行下結(jié)果,靜態(tài)代理比較簡單。動(dòng)態(tài)代理比靜態(tài)代理使用的更廣泛,動(dòng)態(tài)代理在本質(zhì)上,代理類不用我們來管,我們完全交給工具去生成代理類即可。動(dòng)態(tài)代理一般有兩種方式:JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理。

  1. JDK 動(dòng)態(tài)代理

既然動(dòng)態(tài)代理不需要我們?nèi)?chuàng)建代理類,那我們只需要編寫一個(gè)動(dòng)態(tài)處理器就可以了。真正的代理對(duì)象由 JDK 在運(yùn)行時(shí)為我們動(dòng)態(tài)的來創(chuàng)建。

/**

  • 動(dòng)態(tài)代理處理類

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class JdkProxyHandler {

    /**

    • 用來接收真實(shí)明星對(duì)象
      */
      private Object realStar;

    /**

    • 通過構(gòu)造方法傳進(jìn)來真實(shí)的明星對(duì)象
    • @param star star
      */
      public JdkProxyHandler(Star star) {
      super();
      this.realStar = star;
      }

    /**

    • 給真實(shí)對(duì)象生成一個(gè)代理對(duì)象實(shí)例

    • @return Object
      */
      public Object getProxyInstance() {
      return Proxy.newProxyInstance(realStar.getClass().getClassLoader(),
      realStar.getClass().getInterfaces(), (proxy, method, args) -> {

               System.out.println("代理先進(jìn)行談判……");
               // 唱歌需要明星自己來唱
               Object object = method.invoke(realStar, args);
               System.out.println("演出完代理去收錢……");
      
               return object;
           });
      

    }
    }

這里說一下 Proxy.newProxyInstance() 方法,該方法接收三個(gè)參數(shù):第一個(gè)參數(shù)指定當(dāng)前目標(biāo)對(duì)象使用的類加載器,獲取加載器的方法是固定的;第二個(gè)參數(shù)指定目標(biāo)對(duì)象實(shí)現(xiàn)的接口的類型;第三個(gè)參數(shù)指定動(dòng)態(tài)處理器,執(zhí)行目標(biāo)對(duì)象的方法時(shí),會(huì)觸發(fā)事件處理器的方法。網(wǎng)上針對(duì)第三個(gè)參數(shù)的寫法都是 new 一個(gè)匿名類來處理,我這直接用的 Java8 里面的 lamda 表達(dá)式來寫的,都一樣。底層原理使用的是反射機(jī)制。接下來寫一個(gè)客戶端程序來測(cè)試下。

/**

  • 測(cè)試客戶端

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class Client {

    /**

    • 測(cè)試JDK動(dòng)態(tài)代理結(jié)果

    • @param args args
      */
      public static void main(String[] args) {
      Star realStar = new RealStar();
      // 創(chuàng)建一個(gè)代理對(duì)象實(shí)例
      Star proxy = (Star) new JdkProxyHandler(realStar).getProxyInstance();

      proxy.sing();
      }
      }

可以看出,創(chuàng)建一個(gè)真實(shí)的對(duì)象,送給 JdkProxyHandler 就可以創(chuàng)建一個(gè)代理對(duì)象了。

我們對(duì) JDK 動(dòng)態(tài)代理做一個(gè)簡單的總結(jié):相對(duì)于靜態(tài)代理,JDK 動(dòng)態(tài)代理大大減少了我們的開發(fā)任務(wù),同時(shí)減少了對(duì)業(yè)務(wù)接口的依賴,降低了耦合度。JDK 動(dòng)態(tài)代理是利用反射機(jī)制生成一個(gè)實(shí)現(xiàn)代理接口的匿名類,在調(diào)用具體方法前調(diào)用InvokeHandler 來處理。但是 JDK 動(dòng)態(tài)代理有個(gè)缺憾,或者說特點(diǎn):JDK 實(shí)現(xiàn)動(dòng)態(tài)代理需要實(shí)現(xiàn)類通過接口定義業(yè)務(wù)方法。也就是說它始終無法擺脫僅支持 interface 代理的桎梏,因?yàn)樗脑O(shè)計(jì)就注定了這個(gè)遺憾。

  1. CGLIB 動(dòng)態(tài)代理

由上面的分析可知,JDK 實(shí)現(xiàn)動(dòng)態(tài)代理需要實(shí)現(xiàn)類通過接口定義業(yè)務(wù)方法,那對(duì)于沒有接口的類,如何實(shí)現(xiàn)動(dòng)態(tài)代理呢,這就需要 CGLIB 了。

CGLIB 采用了非常底層的字節(jié)碼技術(shù),其原理是通過字節(jié)碼技術(shù)為一個(gè)類創(chuàng)建子類,并在子類中采用方法攔截的技術(shù)攔截所有父類方法的調(diào)用,順勢(shì)織入橫切邏輯。但因?yàn)椴捎玫氖抢^承,所以不能對(duì)final修飾的類進(jìn)行代理。我們來寫一個(gè) CBLIB 代理類。

/**

  • cglib代理處理類

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class CglibProxyHandler implements MethodInterceptor {

    /**

    • 維護(hù)目標(biāo)對(duì)象
      */
      private Object target;

    public Object getProxyInstance(final Object target) {
    this.target = target;
    // Enhancer類是CGLIB中的一個(gè)字節(jié)碼增強(qiáng)器,它可以方便的對(duì)你想要處理的類進(jìn)行擴(kuò)展
    Enhancer enhancer = new Enhancer();
    // 將被代理的對(duì)象設(shè)置成父類
    enhancer.setSuperclass(this.target.getClass());
    // 回調(diào)方法,設(shè)置攔截器
    enhancer.setCallback(this);
    // 動(dòng)態(tài)創(chuàng)建一個(gè)代理類
    return enhancer.create();
    }

    @Override
    public Object intercept(Object object, Method method, Object[] args,
    MethodProxy methodProxy) throws Throwable {

     System.out.println("代理先進(jìn)行談判……");
     // 唱歌需要明星自己來唱
     Object result = methodProxy.invokeSuper(object, args);
     System.out.println("演出完代理去收錢……");
     return result;
    

    }
    }

使用 CGLIB 需要實(shí)現(xiàn) MethodInterceptor 接口,并重寫intercept 方法,在該方法中對(duì)原始要執(zhí)行的方法前后做增強(qiáng)處理。該類的代理對(duì)象可以使用代碼中的字節(jié)碼增強(qiáng)器來獲取。接下來寫個(gè)客戶端測(cè)試程序。

/**

  • 測(cè)試客戶端

  • @author shengwu ni

  • @date 2018-12-08
    */
    public class Client {

    /**

    • 測(cè)試Cglib動(dòng)態(tài)代理結(jié)果

    • @param args args
      */
      public static void main(String[] args) {
      Star realStar = new RealStar();
      Star proxy = (Star) new CglibProxyHandler().getProxyInstance(realStar);

      proxy.sing();
      }
      }

這個(gè)客戶端測(cè)試程序和 JDK 動(dòng)態(tài)代理的邏輯一模一樣,所以也可以看出,代理模式中的動(dòng)態(tài)代理,其實(shí)套路都是相同的,只是使用了不同的技術(shù)而已。

我們也對(duì) CGLIB 動(dòng)態(tài)代理做一下總結(jié):CGLIB 創(chuàng)建的動(dòng)態(tài)代理對(duì)象比 JDK 創(chuàng)建的動(dòng)態(tài)代理對(duì)象的性能更高,但是 CGLIB 創(chuàng)建代理對(duì)象時(shí)所花費(fèi)的時(shí)間卻比 JDK 多得多。所以對(duì)于單例的對(duì)象,因?yàn)闊o需頻繁創(chuàng)建對(duì)象,用 CGLIB 合適,反之使用JDK方式要更為合適一些。同時(shí)由于 CGLIB 由于是采用動(dòng)態(tài)創(chuàng)建子類的方法,對(duì)于final修飾的方法無法進(jìn)行代理。

當(dāng)然了,不管是哪種動(dòng)態(tài)代理技術(shù),在上面的代碼里,要代理的類中可能不止一種方法,有時(shí)候我們需要對(duì)特定的方法進(jìn)行增強(qiáng)處理,所以可以對(duì)傳入的 method 參數(shù)進(jìn)行方法名的判斷,再做相應(yīng)的處理。

  1. Spring AOP 采用哪種代理?

JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理均是實(shí)現(xiàn) Spring AOP 的基礎(chǔ)。對(duì)于這一塊內(nèi)容,面試官問的比較多,他們往往更想聽聽面試者是怎么回答的,有沒有看過這一塊的源碼等等。

針對(duì)于這一塊內(nèi)容,我們看一下 Spring 5 中對(duì)應(yīng)的源碼是怎么說的。

public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
// 判斷目標(biāo)類是否是接口或者目標(biāo)類是否Proxy類型,若是則使用JDK動(dòng)態(tài)代理
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 配置了使用CGLIB進(jìn)行動(dòng)態(tài)代理或者目標(biāo)類沒有接口,那么使用CGLIB的方式創(chuàng)建代理對(duì)象
return new ObjenesisCglibAopProxy(config);
}
else {
// 上面的三個(gè)方法沒有一個(gè)為true,那使用JDK的提供的代理方式生成代理對(duì)象
return new JdkDynamicAopProxy(config);
}
}
//其他方法略……
}

從上述源碼片段可以看出,是否使用 CGLIB 是在代碼中進(jìn)行判斷的,判斷條件是 config.isOptimize()、config.isProxyTargetClass() 和 hasNoUserSuppliedProxyInterfaces(config)。

其中,config.isOptimize() 與 config.isProxyTargetClass()默認(rèn)返回都是 false,這種情況下判斷結(jié)果就由

hasNoUserSuppliedProxyInterfaces(config)的結(jié)果決定了。
簡單來說,
hasNoUserSuppliedProxyInterfaces(config) 就是在判斷代理的對(duì)象是否有實(shí)現(xiàn)接口,有實(shí)現(xiàn)接口的話直接走 JDK 分支,即使用 JDK 的動(dòng)態(tài)代理。

所以基本上可以總結(jié)出 Spring AOP 中的代理使用邏輯了:如果目標(biāo)對(duì)象實(shí)現(xiàn)了接口,默認(rèn)情況下會(huì)采用 JDK 的動(dòng)態(tài)代理實(shí)現(xiàn) AOP;如果目標(biāo)對(duì)象沒有實(shí)現(xiàn)了接口,則采用 CGLIB 庫,Spring 會(huì)自動(dòng)在 JDK 動(dòng)態(tài)代理和 CGLIB 動(dòng)態(tài)代理之間轉(zhuǎn)換。

當(dāng)然,源碼我也沒讀那么深,暫且就只能寫到這,后面深入了,有新的見解再給大家分享。還記得文章開頭的幾個(gè)問題嗎?相信你讀到這里,心中應(yīng)該已經(jīng)有了答案了。

如果覺得對(duì)自己有幫助,可以轉(zhuǎn)發(fā)給更多的伙伴們。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,501評(píng)論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,673評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,610評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,939評(píng)論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,668評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,004評(píng)論 1 329
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,001評(píng)論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,173評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,705評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,426評(píng)論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,656評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,139評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,833評(píng)論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,247評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,580評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,371評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,621評(píng)論 2 380

推薦閱讀更多精彩內(nèi)容