1. 程式人生 > 其它 >如果拋開 Spring,如何自己實現 AOP?面試必問。。。

如果拋開 Spring,如何自己實現 AOP?面試必問。。。

作者:張喜碩
來源:https://segmentfault.com/a/1190000019148468

引言

翻開to-do,註解認證中答應大家要講解代理模式。

正好遇到了一道這樣的題:拋開Spring來說,如何自己實現Spring AOP?

就喜歡這樣的題,能把那些天天寫增刪改查從來不思考的人給PK下去,今天就和大家一切學習代理模式與Spring AOP。推薦一個 Spring Boot 基礎教程及實戰示例:
https://github.com/javastacks/javastack

代理與裝飾器

場景描述

代理,即替代之意,可替代所有功能,即和原類實現相同的規範

代理模式和裝飾器模式很像,之前的裝飾器講的不是很好,這裡換個例子再講一遍。

寧靜的午後,來到咖啡館,想喝一杯咖啡。

基礎實現

給你一個咖啡介面:

public interface Coffee {

    /**
     * 列印當前咖啡的原材料,即咖啡裡有什麼
     */
    void printMaterial();
}

一個預設的苦咖啡的實現:

public class BitterCoffee implements Coffee {

    @Override
    public void printMaterial() {
        System.out.println("咖啡");
    }
}

預設的點餐邏輯:

public class Main {

    public static void main(String[] args) {
        Coffee coffee = new BitterCoffee();
        coffee.printMaterial();
    }
}

點一杯咖啡。

裝飾器模式

優雅的服務生把咖啡端了上來,抿了一口,有些苦。

想加點糖,對服務生說:“您好,請為我的咖啡加些糖”。

/**
 * 糖裝飾器,用來給咖啡加糖
 */
public class SugarDecorator implements Coffee {

    /**
     * 持有的咖啡物件
     */
    private final Coffee coffee;

    public SugarDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public void printMaterial() {
        System.out.println("糖");
        this.coffee.printMaterial();
    }
}

然後服務生就拿走了我的咖啡,去使用SugarDecorator為咖啡加糖,最後把加好糖的咖啡給我。

public class Main {

    public static void main(String[] args) {
        Coffee coffee = new BitterCoffee();
        coffee = new SugarDecorator(coffee);
        coffee.printMaterial();
    }
}

看一看咖啡的成分,對的,確實加上了糖!

注意看這兩行:

Coffee coffee = new BitterCoffee();        // 點了一杯苦咖啡
coffee = new SugarDecorator(coffee);       // 給咖啡加了點糖

裝飾器模式適合什麼場景,我有一個物件,但是這個物件的功能不能令我滿意,我就拿裝飾器給他裝飾一下。

代理模式

週末了,又抱著iPad來到了咖啡館,準備享受一個寧靜的下午。

“先生,請問您要喝點什麼?”一旁禮貌的服務生上前問道。

上次點的咖啡太苦了,這次直接要個加糖的吧。

“我要一杯加糖咖啡。”

public class CoffeeWithSugar implements Coffee {

    private final Coffee coffee;

    public CoffeeWithSugar() {
        this.coffee = new BitterCoffee();
    }

    @Override
    public void printMaterial() {
        System.out.println("糖");
        this.coffee.printMaterial();
    }
}

這是加糖咖啡,其實內部仍然是咖啡,只是加了些配方,就產生了一種新類,一種新的可以在選單上呈現的飲品。

點咖啡:

public class Main {

    public static void main(String[] args) {
        Coffee coffee = new CoffeeWithSugar();
        coffee.printMaterial();
    }
}

正合我意,在咖啡的陪伴下度過了一個美好的下午。

差別

故事講完了,兩者實現的都是對原物件的包裝,持有原物件的例項,差別在於對外的表現。

裝飾器模式:點了咖啡,發現太苦了,不是自己想要的,然後用裝飾器加了點糖。

Coffee coffee = new BitterCoffee();
coffee = new SugarDecorator(coffee);

代理模式:直接就點的加糖咖啡。

Coffee coffee = new CoffeeWithSugar();

很細微的差別,希望大家不要弄混。

批評

去看代理模式相關的資料,五花八門,怎麼理解的都有。

還有,網上許多設計模式的文章都是你抄我、我抄你,一個錯了,全都錯了。

我覺得我需要糾正一下。誰說代理模式一定要用介面的啊?代理模式時設計模式,設計模式不分語言,假如一門語言中沒有介面,那它就不能代理模式了嗎?只是Java中的介面可以讓我們符合依賴倒置原則進行開發,降低耦合。用抽象類可以嗎?可以。用類繼承可以嗎?也可以。

思想明白了,用什麼寫還不是像玩一樣?

另外,設計模式系列面試題和答案全部整理好了,微信搜尋​Java技術棧,在後臺傳送:面試,​可以線上閱讀。

AOP

設計模式是思想,所以我上面說的代理模式不是僅適用於介面便與Spring AOP息息相關。

AOPAspect Oriented Programming,面向切面程式設計,是面向物件程式設計的補充。如果你不明白這句話,好好去學學面向物件就知道為什麼了。

我們會宣告切面,即切在某方法之前、之後或前後都執行。而Spring AOP的實現就是代理模式。

場景

正好最近寫過簡訊驗證碼,就拿這個來當例子吧。

public interface SMSService {

    void sendMessage();
}
public class SMSServiceImpl implements SMSService {

    @Override
    public void sendMessage() {
        System.out.println("【夢雲智】您正在執行重置密碼操作,您的驗證碼為:1234,5分鐘內有效,請不要將驗證碼轉發給他人。");
    }
}

主函式:

public class Main {

    public static void main(String[] args) {
        SMSService smsService = new SMSServiceImpl();
        smsService.sendMessage();
        smsService.sendMessage();
    }
}

費用統計

老闆改需求了,發驗證碼要花錢,老闆想看看一共在簡訊上花了多少錢。

正常按Spring的思路,肯定是宣告一個切面,來切發簡訊的方法,然後在切面內統計簡訊費用。

只是現在沒有框架,也就是這道題:拋開Spring來說,如何自己實現Spring AOP?

寫框架考慮的自然多些,我上文講的代理是靜態代理,編譯期間就決定好的。而框架實現卻是動態代理,需要在執行時生成代理物件,因為需要進行類掃描,看看哪些個類有切面需要生成代理物件。

JDK動態代理

編寫一個統計簡訊費用的類實現InvocationHandler介面。

寫到這,終於明白為什麼每次後臺Debug的時候都會跳轉到invoke方法。

public class MoneyCountInvocationHandler implements InvocationHandler {

    /**
     * 被代理的目標
     */
    private final Object target;

    /**
     * 內部儲存錢的總數
     */
    private Double moneyCount;

    public MoneyCountInvocationHandler(Object target) {
        this.target = target;
        this.moneyCount = 0.0;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = method.invoke(target, args);
        moneyCount += 0.07;
        System.out.println("傳送簡訊成功,共花了:" + moneyCount + "元");
        return result;
    }
}

將主函式裡的smsService替換為使用MoneyCountInvocationHandler處理的代理物件。

public class Main {

    public static void main(String[] args) {
        SMSService smsService = new SMSServiceImpl();
        smsService = (SMSService) Proxy.newProxyInstance(Main.class.getClassLoader(),
                                            new Class[]{SMSService.class},
                                            new MoneyCountInvocationHandler(smsService));
        smsService.sendMessage();
        smsService.sendMessage();
    }
}

根據InvocationHandler中的invoke方法動態生成一個類,該類實現SMSService介面,代理物件,就是用這個類例項化的。

AOP實現

上面的都實現了?寫一個AOP是不是也不是難事?

主函式的程式碼,應該放在IOC容器初始化中,掃描包,去看看哪些個類需要生成代理物件,然後構造代理物件到容器中。

然後在invoke方法裡,把統計費用的邏輯改成切面的邏輯不就好了嗎?

不足分析

結束了嗎?當然沒有,上面的方法實現僅對介面有效。

因為JDK的動態代理,是生成一個實現相應 介面 的代理類。但是Spring又不是隻能通過介面注入。

@Autowired
private Type xxxxx;

Spring@Autowired是通過宣告的型別去容器裡找符合的物件然後注進來的,介面是型別,類不也是型別嗎?

@Autowired
private SMSService smsService;

這樣能注進來。

@Autowired
private SMSServiceImpl smsService;

這樣呢?也能注進來。

所以,JDK動態代理針對直接注入類型別的,就代理不了。

cglib動態代理

自古以來,從來都是時勢造英雄,而不是英雄創造了時代。

出現了問題,自然會有英雄出來解決。拯救世界的就是cglib

JDK動態代理解決不了的,統統交給cglib

就這個來說:

@Autowired
private SMSServiceImpl smsService;

不是使用介面注入的,JDK動態代理解決不了。cglib怎麼解決的呢?它會根據當前的類,動態生成一個子類,在子類中織入切面邏輯。

然後使用子類物件代理父類物件。這就是為什麼我上面說:代理模式,不要拘泥於介面。

所以織入成功的,都是子類能把父類覆蓋的方法。

所以cglib也不是萬能的,方法是final的,子類重寫不了,它當然也無計可施了。

總結

讀書讀的是什麼?是真正理解作者的思想,明白作者想歌頌什麼、批判什麼。

框架學的是什麼?不只是為了提高開發效率,而是在使用的時候,就像與設計者交流一樣,能真正明白框架設計者的思想,才算用明白一款框架。

如果我們都能做到這般,又何愁設計不出一款真正屬於自己的框架呢?

近期熱文推薦:

1.1,000+ 道 Java面試題及答案整理(2021最新版)

2.別在再滿屏的 if/ else 了,試試策略模式,真香!!

3.臥槽!Java 中的 xx ≠ null 是什麼新語法?

4.Spring Boot 2.6 正式釋出,一大波新特性。。

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

覺得不錯,別忘了隨手點贊+轉發哦!