由AOP引發的幾點思考

畢業面試那會兒,被問最多的問題便是:請你解釋一下什麼是AOP思想。當時最喜歡的回答方式是先將英文全稱給呈現出來“威懾”下面試官,即——Aspect Oriented Programming。然後把網上搜集的各種解釋,使用場景理直氣壯地背一遍。這樣的回答能唬住一些對應屆生要求不高的面試官,但真遇上愛刨根問底的大佬就該GG了。最近在項目中用到了AOP,想把幾個思考點總結一下。
在講AOP之前,首先先回顧一下POP——Process Oriented Programming以及OOP——Object Oriented Programming

  • POP與OOP

    • 對於一個問題,給出解決問題的所需的步驟,POP是一種以功能實現爲導向的編程思想,換句話說:功能性的目標實現了就行。然而OOP注重封裝,強調實現過程中的模塊化,對象化,將對象內部的屬性和外部分開。用大家租房時可能遇到的戶型來說,POP偏向於"開放式房型",佈局中有牀,竈臺,浴缸,沙發等各類功能的事物。作爲一個整體提供人們住房的實現。而OOP偏向於傳統的戶型,衛生間,廚房,臥室,客廳等具體事物之間有門隔開,作爲整體起到“房子”該有的功能和效果,但彼此之間又相對獨立,具有較低的耦合性。這樣的房子也能正常居住。
    • POP的設計,節約了空間成本(無需“門”的設計),這種設計在早期計算機配置低,內存小的情況下,是一種以時間換空間的良好設計。但這種設計的房子使得各個事物時間相互暴露互相串味,整理佈局來看,也顯得有些雜亂無章。OOP的戶型則提供了更加優雅的設計,使得各個事物都有自己“該待”的地方(類的概念)。不同的類之間無需知道對方的細節,是需提供彼此之間自身的功能和屬性(方法調用等)即可,各類各司其職,達到整體上服務租客的效果。
    • 當然不能因爲我是一名Java開發而肯定OOP思想的同時否認POP思想,兩種思想都是不同的時期人類思考的產物,OOP和POP相對來說是整體和局部的概念,這也是從方法論的角度來看待兩者。POP之間互相暴露的功能實現就像OOP實現中某個類,同類屬性和方法是有相互瞭解的權利的。
    • 由OOP編程思想主導的項目裏,充斥着衆多相互依賴同時相互隔離的對象,將一些數據(屬性)和算法(方法)封裝在類裏面,使得系統更加安全,也更便於後期修改維護。但隨着系統的複雜性的增強,應用會進行相應的升級。OOP設計思想也逐漸開始暴露弊端。
  • 場景一

    • 背景A:ClassA,ClassB以及ClassC,有各自需實現的業務方法。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {

    public void doSomethingInA(){
        // Biz code omitted here.
    }
}
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:41
 */
public class ClassB {

    public void doSomethingInB(){
         // Biz code omitted here.

    }

    private void checkIfLogIn() {

    }
}
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:41
 */
public class ClassC {

    public void doSomethingInC(){
        // Biz code omitted here.
    }

}
  • 背景B: 各個類的各個方法執行前需增加“用戶是否登錄”的權限校驗功能。
    • A猿靈機一動。給每一個類的每一個方法原代碼之前加入瞭如下的判斷。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {
    
    private  User user;

    public void doSomethingInA(){
        if (!user.isStatus()){
            return;
        }
         // Biz code omitted here.
    }
}
  • 背景B: 各個類的各個方法執行前需增加“用戶是否登錄”的權限校驗功能。
    • B猿一陣嘲笑:呵呵,這不是侵入原代碼了麼?要是再來幾個其他的功能入侵怎麼辦。下面是他的修改方案。將原“非法入侵”部分抽離成另一個私有方法進行調用。其他幾個類的方法也進行了同樣的處理。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {

    private  User user;

    public void doSomethingInA(){
        
        checkIfLogIn();
         // Biz code omitted here.
    }

    private void checkIfLogIn() {
        if (user.isStatus()){
            return;
        }
    }
}
  • 背景B: 各個類的各個方法執行前需增加“用戶是否登錄”的權限校驗功能。
    • C猿看了搖搖頭,心想:從整體來看,這三個類中被抽離出來的方法實現的功能不是一樣的麼?這對於整個系統來說不還是冗餘代碼麼?於是他有了以下的改方案。利用AOP切面,在需進行用戶登錄校驗的方法上加相應的註解,在不侵入原業務代碼的同時也能實現較大程度的代碼複用。這樣的實現,代碼的擴展性,可維護性也更強。
 /**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 22:40
 */
public class ClassA {

    private  User user;

    @CheckLogInListener
    public void doSomethingInA(){
         // Biz code omitted here.
    }
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 23:04
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CheckLogInListener {
    String name() default "";
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 23:09
 */
@Aspect
@Component
@Slf4j
public class CheckLogInHandler {
    private static final String EXECUTION="@annotation(checkLogInListener)";

    @Before(EXECUTION)
    public void checkIfLogIn(CheckLogInListenercheckLogInListener) {
        // checkIfLogIn code omitted here.
    }
}
  • 背景C: 各個類的各個方法執行前去掉“用戶是否登錄”的權限校驗功能,並加上方法前後的日誌打印以及業務異常失敗重試功能。並要求日誌打印的功能執行順序在也是失敗重試功能之前。
    • 聰明的A猿聽了背景B下C猿的解法後恍然大悟,刷刷刷給出了下面的解法。
 /**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:30
 */
@Service
public class AopTestService {

    @RetryListener(retryForException = Exception.class)
    @LogListener(logLevel = "info")
    public String testForAopOrder(MyInfo myInfo) {
    
        return myInfo.toString();
        
    }
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/29 22:11
 */
@Target(ElementType.METHOD)
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LogListener {

    String logLevel() default "";

}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/29 22:14
 */

@Slf4j
@Aspect
@Component
public class LogHandler {

    private static final String EXECUTION = "@annotation(logListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, LogListener logListener) {
        log.info("LOG-begins. Args={}, logLevel={}", point.getArgs(), logListener.logLevel());
        try {
            Object returnObj = point.proceed();
            log.info("LOG-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Log-ends with error,error={}", throwable);
        }
    }
}
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:17
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RetryListener {

    Class<?>[] retryForException();

    Class<?>[] noRetryForException() default Exception.class;
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:28
 */
@Slf4j
@Aspect
@Component
public class RetryHandler {
    private static final String EXECUTION = "@annotation(retryListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, RetryListener retryListener) {
        log.info("Retry-begins. Args={}, retryForExceptions={}", point.getArgs(), retryListener.retryForException());
        try {
            Object returnObj = point.proceed();
            log.info("Retry-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Retry-ends with error,error={}", throwable);
        }
    }
}
  • 背景C: 各個類的各個方法執行前去掉“用戶是否登錄”的權限校驗功能,並加上方法前後的日誌打印以及業務失敗重試功能。並要求日誌打印的功能執行順序在也是失敗重試功能之前。
    • 請求後的結果是:
      在這裏插入圖片描述
  • 背景C: 各個類的各個方法執行前去掉“用戶是否登錄”的權限校驗功能,並加上方法前後的日誌打印以及業務失敗重試功能。並要求日誌打印的功能執行順序在也是失敗重試功能之前。
    • 看到希望的執行結果,A猿沾沾自喜,但B猿卻提出了A猿的解法有投機取巧的嫌疑:雖然此解法得到了想要的結果,但並沒有指定具體的Aspect之間的執行順序。B猿接過代碼,在兩個Aspect上分別加上了@Order註解。並給出瞭解釋:@Order中的數字代表優先級,數字越小,優先級越高(越先執行)。
/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/29 22:14
 */
@Order(0)
@Slf4j
@Aspect
@Component
public class LogHandler {

    private static final String EXECUTION = "@annotation(logListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, LogListener logListener) {
        log.info("LOG-begins. Args={}, logLevel={}", point.getArgs(), logListener.logLevel());
        try {
            Object returnObj = point.proceed();
            log.info("LOG-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Log-ends with error,error={}", throwable);
        }
    }
}

/**
 * @Author: Rebecca in Shanghai.
 * @Date: 2019/3/30 16:28
 */
@Order(1)
@Slf4j
@Aspect
@Component
public class RetryHandler {
    private static final String EXECUTION = "@annotation(retryListener)";

    @Around(EXECUTION)
    public void doAround(ProceedingJoinPoint point, RetryListener retryListener) {
        log.info("Retry-begins. Args={}, retryForExceptions={}", point.getArgs(), retryListener.retryForException());
        try {
            Object returnObj = point.proceed();
            log.info("Retry-ends. ReturnObj={}", returnObj);
        } catch (Throwable throwable) {
            log.error("Retry-ends with error,error={}", throwable);
        }
    }
}
  • 背景C: 各個類的各個方法執行前去掉“用戶是否登錄”的權限校驗功能,並加上方法前後的日誌打印以及業務失敗重試功能。並要求日誌打印的功能執行順序在也是失敗重試功能之前。
    • 那麼爲什麼在不指定Order的情況下,A猿的實驗結果也是“正確”的呢?C猿通過閱讀源碼給出了下面的解釋:
      • 首先,不指定Order的情況下,所有的Aspect的優先級都是最低的(lowest precedence)
      • 在不指定Order的情況下,Aspect的執行順序遵從目標對象在容器中的註冊順序有關。
      • 這也就表明了面向切面編程的“不可控性”,當同一套代碼部署PROD環境中,如果不指定Order,可能會出現與DEV環境不同的執行順序,導致不可預料的效果。之前在Spring.doc文檔中看過作者形容不指定Order時,不同的Aspect執行的順序,印象很深的一個詞是“Arbitrary”,即Aspect的執行順序是隨意的,不同的jvm對於執行順序都有其隨機算法。不知道這樣理解對不對。但希望今後大家在使用AOP編程時能使用有效的方法控制切入代碼和原有代碼的執行順序,否則後患無窮。
        在這裏插入圖片描述
        在這裏插入圖片描述
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章