Spring學習筆記(六)——AOP篇(上)

前提

這篇博文是這套Spring學習筆記的第六篇——AOP篇(上),主要內容包含Spring AOP的基礎知識及應用,全篇以一個我遇到的真實編程問題的出現、思考及通過Spring AOP得以解決的過程,使大家對AOP的應用場景可以有一個深刻的認識。如果需要了解有關Spring的綜述信息或博文的索引信息,請移步:
《綜述篇》


什麼是AOP?

先從概念上來說,AOP即Aspect Oriented Programing——面向切面的編程,類似的一個概念即OOP(Object Oriented Programing)——面向對象的編程。AOP並不是用來取代OOP的,而是應OOP中某些特定的應用場景而誕生的。

具體的應用場景以下面的例子給出。
《配置篇》的“controller包和UserController”這一小節,我們創建了一個UserController類,其中有個handleLoginRequest函數用來執行用戶登錄的業務邏輯,我們再給它添上一個Register註冊函數,代碼如下:

@Controller  
public class UserController {

    @Autowired  
    private UserService userService;

    @RequestMapping("Login")  
    private void handleLoginRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try (PrintWriter out = response.getWriter()) {
            request.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            response.setCharacterEncoding("utf-8");
            //此處登錄功能的業務邏輯省略
        }
    }

    @RequestMapping("Register")  
    private void handleRegisterRequest(HttpServletRequest request, HttpServletResponse response) throws IOException {
        try (PrintWriter out = response.getWriter()) {
            request.setCharacterEncoding("utf-8");
            response.setContentType("text/html;charset=utf-8");
            response.setCharacterEncoding("utf-8");
            //此處註冊功能的業務邏輯省略
        }
    }
}

可以看到兩個函中有三行重複代碼,它們的作用是設置請求和響應中的字符編碼:

request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
response.setCharacterEncoding("utf-8");

我們可以想象到的是,在工程的規模逐漸擴大後,這種請求處理函數會有很多,如果每個函數前面都有這麼三行重複代碼,很明顯違反了OOP的原則。那麼一般想法就是給這三行代碼提到一個函數中去,每個處理請求的函數再來調用這個函數。但是這樣做只是把重複的三行代碼變成了一行,並不算是“優雅”地解決了這個問題。

我們設想能有一種類似函數監聽器的東西,能在我們指定的函數執行之前先執行這三行代碼。這樣我們就不用在業務邏輯中見到它們了。

Spring AOP就是用來實現類似的功能的,除了上述場景,AOP主要適用於性能監控、日誌記錄等與業務邏輯關係不大的場景中。在傳統的OOP中,一般這類代碼必須加在主要業務邏輯的一端或兩端,通過AOP可以在保證功能的前提下對這些代碼和業務邏輯進行解耦。


AOP的基礎術語

①連接點(JoinPoint)
準確的說這個概念更貼近於“時間上的點”,而不是“代碼順序上的點”。比如函數執行前、執行後和拋出異常時等。

②切點(Pointcut)
切點是連接點的子概念,一個函數可以有多個連接點,但是我們關心的,要對其執行操作的連接點纔是切點。比如上述例子中,處理請求的各函數的“執行前”這個連接點纔是切點,其他的連接點我們不關心,也就不是切點。

③增強(Advice)
即我們在切點上要做的操作。比如上述例子中設置請求和響應中的字符編碼的那三行代碼。

④織入(Weaving)
即AOP將增強和原代碼中的業務邏輯結合起來的過程。

⑤切面(Aspect)
它是切點和增強概念的結合,Spring AOP把切面定義的增強編織到切面定義的切點中。通俗的來說,就是Spring AOP會根據我們的命令在函數運行的特定的點上執行特定的操作。


增強的類型

Spring AOP支持五種增強類型:前置增強、後置增強、環繞增強、異常拋出增強和引介增強。

①前置增強(Before):即在函數調用前施以增強;
②後置增強(AfterReturning):即在函數返回後施以增強;
③環繞增強(Around):即在函數執行前後都施以增強;
④異常拋出增強(AfterThrowing):即在函數拋出異常後施以增強;
⑤引介增強(Introduction):不同於其他增強,引介增強可以對指定的類在運行時動態的實現指定的接口。


基於AspectJ的AOP

AspectJ是一個AOP框架,Spring無縫集成了AspectJ。

注意:使用AspectJ前需要額外添加aspectjtools和cglib的Jar包到庫中。
aspectjtools:鏈接 密碼:du9t
cglib:鏈接 密碼:7tva

我們先給出上述問題的實現代碼,首先是切面類:

@Aspect  //註釋①
public class BeforeHandleRequestAspect{
    @Before("execution(* com.implementist.MyFirstWebApp.controller.*(..))")  //註釋②
    public void beforeHandleRequest(JoinPoint joinPoint){  //註釋③
        Object[] args = joinPoint.getArgs();
        HttpServletRequest request = (HttpServletRequest) args[0];
        HttpServletResponse response = (HttpServletResponse) args[1];

        request.setCharacterEncoding("utf-8");
        response.setContentType("text/html;charset=utf-8");
        response.setCharacterEncoding("utf-8");
    }
}

註釋:
①@Aspect註解標示該類是一個切面類;
②@Before註解標示這是一個前置增強,括號裏的字符串是一個切點表達式,作用是定位com.implementist.MyFirstWebApp.controller包下的所有類中的所有函數。這一行的整體效果是對上述所有函數織入前置增強;
③增強的函數體,即前置增強期間要做的操作。通過定義JoinPoint參數可以訪問切點的信息,如參數列表等。

接着需要對spring-mvc.xml文件做兩部分修改:
①爲<beans>標籤增加aop命名空間的聲明:

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"

       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/context/spring-context-4.3.xsd
          http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
">

②因爲作爲被增強的對象,controller包下面的類在經歷掃描後已經生成了對應的bean,所以還需要添加前置增強切面類的bean和aop自動代理的代碼:

<bean class="com.implementist.MyFirstWebApp.BeforeHandleRequestAspect"/>
<aop:aspectj-autoproxy/>

這樣,Spring AOP就會自動在指定的函數切點上織入前置增強,每當函數執行前,都會先執行增強函數來設置request和response的字符編碼。

切點表達式函數

在上述代碼中,我們看到
@Before("execution(* com.implementist.MyFirstWebApp.controller.*(..))")
這個前置增強註解的參數好像有一個用字符串定義的函數
execution(* com.implementist.MyFirstWebApp.controller.*(..))
這個execution()就是Spring支持的9個切點函數之一,另外8個函數是:@annotation()、args()、@args()、within()、target()、@within()、@target()和this()。

通配符

上述函數中,有的可以使用通配符,AspectJ一共支持三種通配符:
①*匹配單個任意字符串;
②..匹配多個任意字符串,表示類時必須與*連用,即..*;
③+按類型匹配,如execute(* Animal+(..))匹配Animal類及其子類的所有函數。

注意:
①execution()和within()支持全部通配符;
②args()、this()和target()僅支持+通配符;
③其餘的函數不支持任何通配符。

邏輯運算符

和Java中的邏輯運算符相似,上述9個切點表達式函數可以通過邏輯運算符進行邏輯運算,邏輯運算符即①&&表示與;②||表示或;③!表示非
如我需要對controller包下所有類的所有函數中,參數爲request和response的函數織入後置增強:

@AfterReturning("within(com.implementist.MyFirstWebApp.controller.*) and args(request,response)")

各增強對應的註解

前置增強——@Before
後置增強——@AfterReturning
環繞增強——@Around
異常拋出增強——@AfterThrowing
引介增強——@DeclareParents
另外,AspectJ還支持一個Final增強——@After,這個增強相當於異常捕獲模塊的final塊,無論程序正常還是異常退出,都會執行該增強。

各切點表達式函數的使用方法

①execution:最常用的函數,其語法爲:
execution(<修飾符>?<返回類型><函數名>(<參數>)<異常>?)
如 execution(public * com..*.*Function(String,int,..)) 表示權限爲public的com包下的所有類中,名稱以“Function”爲後綴的,前兩個參數分別是String和int型,後面可以有任意多個參數的函數。

注意:在java.lang包下的類直接寫非全限定名即可,如Stringintdouble等;除此之外的類都要寫全限定名,如com.implementist.MyFirstApp.domain.User

②@annotation:表示標註了指定註解的全部函數,參數爲指定註解的全限定名,如
@annotation(com.implementist.MyFirstWebApp.ContainsBugs)表示所有被冠以@ContainsBugs註解的函數。

③args:表示參數是指定的類型的函數,如
args(String,int)表示僅有兩個參數,且分別爲String和int類型的全部函數(順序必須一致);
args(String,int,*)表示僅有三個參數,前兩個參數分別爲String和int類型,第三個參數爲任意類型的全部函數;
args(String,int,..)表示前兩個參數分別爲String和int類型,後面可以有任意個任意類型參數的全部函數。

④@args:表示參數被冠以指定類型註解的函數,如
@args(com.implementist.MyFirstWebApp.ContainsBugs)表示僅有一個參數,且該參數被冠以了@ContainsBugs註解的全部函數。

注意:因爲註解是可以被繼承的,在A◁——B◁——C這樣一個類繼承樹上:
①如果A中有函數被冠以@ContainsBugs註解,在B和C中用@args(com.implementist.MyFirstWebApp.ContainsBugs)做切點匹配不到任何連接點,因爲註解點A高於判斷點B或C;
②反之,在C中有函數被冠以@ContainsBugs註解,在A和B中用@args(com.implementist.MyFirstWebApp.ContainsBugs)做切點都可以匹配到C,因爲註解點C低於判斷點A或B。

⑤within:表示類匹配模式,它可以匹配指定類及其子類中的全部函數,它是靜態植入的,即在程序運行前, 參數最低只能指定到類級別,如
within(com.implementist.MyFirstWebApp.controller.*)表示com.implementist.MyFirstWebApp.controller包下的所有類及其子類的所有函數。

⑥target:表示類匹配模式,它可以匹配指定類及其子類中的全部函數,它是動態植入的,即在程序運行期間, 參數最低只能指定到類級別,如
target(com.implementist.MyFirstWebApp.Utils)表示com.implementist.MyFirstWebApp.Utils及其子類的全部函數。

注意,如果此時通過引介增強等手段爲Utils類或者其子類實現了額外的接口比如Collection接口,那麼其中的size()等函數也會被匹配爲切點。這邊是target動態植入與within靜態植入的不同。

⑦@within:表示被冠以指定註解的類及其子類的全部函數,如
@within(com.implementist.MyFirstWebApp.ContainsBugs)表示被冠以@ContainsBugs註解的類及其子類的全部函數。

⑧@target:表示匹配被冠以指定註解的類的所有函數,如
@within(com.implementist.MyFirstWebApp.ContainsBugs)表示被冠以@ContainsBugs註解的類的全部函數。

⑨this:表示代理類匹配模式,它可以匹配指定類及其子類中的全部函數,它是動態植入的,即在程序運行期間,一般情況下它的效果和target()是等效的

注意,它與target()的不同點在於,當通過引介增強爲目標類動態實現一個接口時,該接口內的函數也會被全部匹配。


後記

這一篇就先到這了,因爲AOP這裏的知識確實多,所以我決定分爲上下兩篇,一可以讓讀者休息一下,不至於一口氣看的太累。二來我也可以先整體瀏覽一下這前半部分,然後休息一下接着寫。

另外,有一些在AOP發展過程中很重要,曾經需要我們手動添加但現在已經被封裝的一些東西我就省略掉了,比如最後只提到了一下的代理這個東西。雖然在AOP中可以自動代理,但是代理這個概念還是很重要的,比如設計模式中就有一個代理模式,有興趣的話可以移步《Java設計模式之代理模式(Proxy)》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章