長話短說Spring(2)之AOP面向切面編程

簡書 Wwwwei
轉載請注明原創出處,謝謝!

前言


上節回顧 長話短說Spring(1)之IoC控制反轉

??上篇文章中,我們介紹了有關Spring IoC控制反轉的概念以及實現,接下來我們將說一說Spring的另一重大特點AOP面向切面編程。同樣,還是老套路,我們將從是什么、怎么做、為什么三個主要方向入手,說清楚AOP到底是個什么東西、Spring 怎么做實現了AOP以及為什么要使用AOP這幾個問題。

什么是AOP?


官方解釋

??我們先來看一下比較官方的解釋。
??AOP,Aspect Oriented Programming的縮寫,意為面向切面編程,通過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。
??依然沒看懂ㄟ( ▔, ▔ )ㄏ。

生活中的AOP

??要理解AOP(面向切面編程),首先我們要明白什么叫做切面。
??舉個生活中的例子,我們都知道一個成功的老板背后一定有一個優秀的秘書,而秘書的職責就是負責調度和安排老板的日程。想象一下這樣一個場景,今天秘書小M將事先安排好的一天日程向老板大B匯報后,老板大B表示同意。那么如無意外,老板大B今天將和往常一樣,按照日程表一樣一樣完成,一天就過去了。但是,秘書小M突然接到通知,一個十分重要的會議臨時決定在午后召開,無奈秘書小M只能將該會議強行插入老板大B已經確認過的日程安排中,然后默默等待老板大B的白眼。

切面

????
??我們注意到秘書小M將臨時會議插入日程的動作,是不是很符合切面這個感覺呢?更形象一點,我們將老板大B確認過的日程想象成一條長面包,而秘書小M突然被告知還有一片火腿(即那個討厭的臨時會議),為了老板能夠吃好,不得已只能用小刀將面包切開后硬生生將火腿塞入。
??可以這樣理解,面向切面就是為了達到目的,動態地將某件事切入到某件事中。

程序中的AOP

??回到代碼層面,關于AOP我在網上博文中發現了這樣一句話:運行時,動態地將代碼切入到類的指定方法或者指定位置。是不是豁然開朗了呢?
??這里還想強調一下運行時動態兩個點,運行時不難理解,如果在程序非運行時我們只要將代碼寫入指定位置再運行就好了,而程序運行時,代碼就無法進行更改了,這也體現了不修改源代碼的意義;動態這個概念我們可以這樣理解,切入的過程并非是事先完成好的,而是在程序運行過程中觸發了某個時機而進行的。
??此處介紹幾個概念,便于讀者理解:
??通知(advice):切入到類指定方法或者指定位置的代碼片段,即需要增加的功能代碼,也就是上述的那片火腿(臨時會議)。
??連接點(join point):程序運行過程中能夠進行插入切面操作的時間點。例如方法調用、異常拋出或字段修改等,可以理解為上述的長面包(老板的整個日程安排)。
??切入點(pointcut):描述一個通知將被切入的一系列連接點的集合,即代碼片段具體切入到哪些類、哪些方法,也就是上述面包切口處(特指午后的日程)。所以說,切入點規定了哪些連接點可以執行哪些通知。
??切面(aspect):AOP中的切面等同于OOP中的類(class),由通知(advice)和切入點(pointcut)組成,其中通知(advice)和切入點(pointcut)既可以是1對1的關系,也可以是1對多的關系。概括的說就是描述了何時何地干何事的基本單元,其中通知(advice)說明了切面干何事,而切入點則說明了切面何時何地切入。
??關于幾者的關系,我們可以這樣理解,通知是在連接點上執行的,但是我們不希望通知應用到所有的連接點,所以引入了切入點來匹配特定的連接點,指名我們所希望通知應用的連接點。
??因此,所謂的AOP(面向切面編程)就是在程序運行過程中的某個時機將代碼片段插入到某些類的指定方法和指定位置;換句話說,秘書接到臨時會議通知時,將臨時會議插入到老板的日程安排中去。

為什么要使用AOP?


??程序的最終目的就是實現業務,但是我們在進行編程的過程中經常會發現除了所謂的業務代碼,還存在數量相當的公共代碼,類似日志、安全驗證、事物、異常處理等問題。這部分代碼重要但是與我們編寫程序要實現的功能沒有關系,具有功能相似、重用性高、使用場景分散等特點。我們姑且稱它們為共性問題
??對大多數程序而言,代碼都是以縱向結構將各個業務模塊串聯從而完成功能的。我們提到的共性問題本身不屬于業務范圍,但是又散落在各個業務模塊間,同實現主功能的代碼相互雜糅在一起,即如下圖所示:

程序邏輯圖

??試想一下,如果將共性問題部分的代碼融入業務代碼中,一旦涉及到對某個共性問題部分的代碼進行更改的時候,例如日志部分發生需求變更,我們可能需要牽涉許許多多其他模塊代碼。這在小規模程序中也許是可以接受的,可能只修改1、2處;但是如果牽涉的地方數量過多,特別是應用在中大型規模程序中,我們甚至會為了小小的一個功能,修改上千、上萬處。這樣的方式是十分糟糕的,不僅費時費力,可能還會引起一些不必要的麻煩(回歸錯誤、結構混亂等等)。
??AOP的就是為了解決這類共性問題,將散落在程序中的公共部分提取出來,以切面的形式切入業務邏輯中,使程序員只專注于業務的開發,從事務提交等與業務無關的問題中解脫出來
AOP程序邏輯圖

AOP的好處

??解耦:AOP將程序中的共性問題進行了剝離,毫無疑問地降低了各個業務模塊和共性問題之間的耦合。
??重用性:共性問題散落于業務邏輯的各處,十分難維護,使用AOP進行提取后,能夠將相似功能的共性問題收斂,減少重復代碼,提高了代碼的重用性。
??拓展性:對于一個程序而言,迭代的重心一定在于業務和功能上。AOP使得每當發生變更時,可以只關注業務邏輯相關的代碼,而減少共性問題上帶來的變化,大大降低了程序未來拓展的成本。

Spring如何實現AOP?


什么是Spring AOP?

??通過上述介紹,我們了解到AOP的重心在于定義基本結構完成切面切入兩部分,解決了它們,我們就能方便快捷的使用AOP思想進行編程了。
??Spring AOP就是負責完成AOP相關工作的框架,它將切面所定義的橫切邏輯切入到切面所指定的連接點中。框架的目的就是將復雜的事情變得簡單易用,所以其主要工作分成了如下兩點:
??1.提供相應的數據結構來定義AOP所需基本結構,例如通知、連接點、切入點、切面等。
??2.封裝切面切入的相關工作,提供相應接口給用戶。封裝的內容主要包括如何通過切面(切入點和通知)定位到特定的連接點;如何將切面中的功能代碼植入到特定的連接點中等等;提供相應接口主要包括配置文件、注解等用戶能夠使用的工具。
??這里想提一下,在 Spring AOP 中,連接點(join point)總是方法的執行點, 即只有方法連接點。所以我們可以認為,在 Spring 中所有的方法都可以是連接點。

怎么使用Spring AOP?

??由于Spring對AOP的封裝,使得我們可以十分方便的使用,我們只需要定義切面,即定義Advice通知和Pointcut切入點

(1)Spring AOP中Pointcut切入點

??使用@Pointcut("execution()")注解定義切入點,其中execution的格式如下所示:

execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern) throws-pattern?) 

??其中,除ret-type-pattern和name-pattern之外,其他都是可選的,各部分具體含義如下:
??1.modifiers-pattern:方法的操作權限
??2.ret-type-pattern:返回值
??3.declaring-type-pattern:方法所在的包
??4.name-pattern:方法名
??5.parm-pattern:參數名
??6.throws-pattern:異常
??為了更加理解,舉一個簡單的execution示例,如下所示:

execution示例
(2)Spring AOP中Advice通知
  • before advice:前置通知,由@Before注解定義,表示在 join point 前被執行的advice。(雖然before advice是在 join point 前被執行,但是它并不能夠阻止 join point 的執行, 除非發生了異常,即在before advice代碼中, 不能人為地決定是否繼續執行join point中的代碼)
  • after return advice:后置通知,由@AfterReturning注解定義,表示在一個 join point 正常返回后執行的advice。
  • after throwing advice:異常通知,由@AfterThrowing注解定義,表示當一個 join point 拋出異常后執行的advice。
  • after(final) advice:最終通知,由@After注解定義,表示無論一個join point是正常退出還是發生了異常,都會被執行的advice。
  • around advice:環繞通知,由@Around注解定義,表示在join point 前和joint point退出后都執行的 advice,是最常用的advice。
(3)舉個例子

??首先,我們創建一個簡單的UserService,作為示例的業務模塊,代碼如下:

package com.demo.aop;

import org.springframework.stereotype.Service;

/**
 * Created by wwwwei on 17/8/14.
 */
@Service
public class UserService {
    //創建用戶
    public void createUser(String userName) {
        System.out.println("創建用戶 " + userName);
    }

    //刪除用戶
    public void deleteUser(String userName) {
        System.out.println("刪除用戶 " + userName);
    }

    //更新用戶
    public void updateUser(String userName) {
        System.out.println("更新用戶 " + userName);
        throw new RuntimeException("更新用戶拋出異常");
    }
}

??其次,定義切面數據結構,即通知(需要增加的功能代碼片段)和切入點(切入的時間點),代碼如下:

package com.demo.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

/**
 * Created by wwwwei on 17/8/11.
 */
@Component
@Aspect
public class UserAspect {
    //定義切入點
    @Pointcut("execution(* com.demo.aop.*Service*.*(..))")
    public void pointCut() {
    }

    //前置通知 @Before(value="execution(public * *(..))")
    @Before("pointCut()")
    public void mybefore() {
        System.out.println("前置通知");
    }

    //后置通知 @AfterReturning(value="execution(public * *(..))")
    @AfterReturning(pointcut = "pointCut()")
    public void myafterReturning() {
        System.out.println("后置通知");
    }

    //異常通知 @AfterThrowing(value="execution(public * *(..))")
    @AfterThrowing(pointcut = "pointCut()", throwing = "error")
    public void myafterThrowing() {
        System.out.println("異常通知");
    }

    //環繞通知 @Around(value="execution(public * *(..))")
    @Around("pointCut()")
    public void myAround(ProceedingJoinPoint jp) {
        System.out.println("環繞前通知");
        try {
            jp.proceed();
        } catch (Throwable e) {
            e.printStackTrace();
        }
        System.out.println("環繞后通知");

    }

    //最終通知 @After(value="execution(public * *(..))")
    @After("pointCut()")
    public void myafterLogger() {
        System.out.println("最終通知");
    }
}

??最后,編寫測試類測試,代碼如下:

package com.demo.aop;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * Created by wwwwei on 17/8/14.
 */
public class UserTest {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                "applicationContext.xml");
        UserService userService = context.getBean(UserService.class);
        userService.createUser("測試用戶");
    }
}

??運行結果如下,可以發現在業務模塊中成功切入了我們定義的代碼片段。


運行結果
AOP的實現

??AOP的實現是基于代理機制的,根據不同實現方式主要分為兩類:
??1.靜態代理,AOP框架會在編譯階段生成AOP代理類,即在編譯器和類裝載期實現切入的工作,但是這種方式需要特殊的Java編譯器和類裝載器。AspectJ框架就是采用這種方式實現AOP。
??2.動態代理,AOP框架不會去修改字節碼,而是在內存中臨時為方法生成一個AOP對象,這個AOP對象包含了目標對象的全部方法,并且在特定的連接點(切入點)做了添加通知(advice)處理,并回調原對象的方法。
??與AspectJ的靜態代理不同,Spring AOP使用動態代理,通過JDK Proxy和CGLIB Proxy兩種方法實現代理。兩種方式的選擇與目標對象有關:

  • 如果目標對象沒有實現任何接口,那么Spring將使用CGLIB來實現代理。CGLIB是一個開源項目,它是一個強大的,高性能,高質量的Code生成類庫,它可以在運行期擴展Java類與實現Java接口。
  • 如果目標對象實現了一個以上的接口,那么Spring將使用JDK Proxy來實現代理,因為Spring默認使用的就是JDK Proxy,并且JDK Proxy是基于接口的。這也是Spring提倡的面向接口編程。當然,你也可以強制使用CGLIB來進行代理,但是這樣可能會造成性能上的下降。
    ??感謝以下博文,寫作時作為參考借鑒。
    ??Spring AOP的實現原理
    ??徹底征服 Spring AOP 之 理論篇
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,908評論 18 139
  • 本章內容: 面向切面編程的基本原理 通過POJO創建切面 使用@AspectJ注解 為AspectJ切面注入依賴 ...
    謝隨安閱讀 3,193評論 0 9
  • AOP,也就是面向方面編程或者說面向面編程,是一種很重要的思想。在企業級系統中經常需要打印日志、事務管理這樣針對某...
    樂百川閱讀 915評論 0 8
  • TFBOYS 中國內地少年偶像組合,出道時平均年齡僅有13歲,是中國年齡最小的偶像團體,兩年內又以極快的速度在傳統...
    十年易7閱讀 350評論 1 0
  • 記得2005、2006年的時候,博客正盛行。恰好那時候的工作是在一家音像發行公司,除去購買一些紀錄片版權后需要發片...
    愛熙世界閱讀 1,401評論 3 4