SpringMVC干貨系列:從零搭建SpringMVC+mybatis(四):Spring兩大核心之AOP學習

前言

上一篇我們介紹了Spring的核心概念DI,DI有助與應用對象之間的解耦。今天我們就來介紹下另一個非常核心的概念,面向切面編程AOP。

正文

在軟件開發中,散布于應用中多處的功能被稱為橫切關注點(cross-cutting concern)。通常來講,這些橫切關注點從概念上是與應用的業務邏輯相分離的。比如:日志、聲明式事物、安全和緩存。這些東西都不是我們平時寫代碼的核心功能,但許多地方都要用到。

把這些橫切關注點與業務相分離正是面向切面編程(AOP)索要解決的問題。

簡單的說就是把這些許多地方都要用到,但又不是核心業務的功能,單獨剝離出來封裝,通過配置指定要切入到指定的方法中去。

什么是面向切面編程


如上圖所示,這就是橫切關注點的概念,水平的是核心業務,這些切入的箭頭就是我們的橫切關注點。
橫切關注點可以被模塊化為特殊的類,這些類被稱為切面(aspect)。這樣做有兩個好處:

  • 首先,現在每個關注點都集中于一個地方,而不是分割到多處代碼中
  • 其次,服務模塊更簡潔,因為它們只包含主要關注點(或核心功能)的代碼,而次要關注點的代碼被轉移到切面中了。

定義AOP術語

為了理解AOP,我們必須先了解AOP的相關術語,很簡單不難:

通知(Advice)
在AOP中,切面的工作被稱為通知。通知定義了切面“是什么”以及“何時”使用。除了描述切面要完成的工作,通知還解決了何時執行這個工作的問題。

Spring切面可以應用5種類型的通知:

  • 前置通知(Before):在目標方法被調用之前調用通知功能
  • 后置通知(After):在目標方法完成之后調用通知,此時不會關心方法的輸出是什么
  • 返回通知(After-returning):在目標方法成功執行之后調用通知
  • 異常通知(After-throwing):在目標方法拋出異常后調用通知
  • 環繞通知(Around):通知包裹了被通知的方法,在被通知的方法調用之前和調用之后執行自定義的行為

連接點(Join point)
連接點是在應用執行過程中能夠插入切面的一個點。這個點可以是調用方法時、拋出異常時、甚至修改一個字段時。切面代碼可以利用這些點插入到應用的正常流程之中,并添加行為。

切點(Pointcut):
如果說通知定義了切面“是什么”和“何時”的話,那么切點就定義了“何處”。比如我想把日志引入到某個具體的方法中,這個方法就是所謂的切點。

切面(Aspect)
切面是通知和切點的結合。通知和切點共同定義了切面的全部內容———他是什么,在何時和何處完成其功能。

引入(Introduction)
引入允許我們向現有的類添加新的方法和屬性(Spring提供了一個方法注入的功能)。

織入(Weaving)
把切面應用到目標對象來創建新的代理對象的過程,織入一般發生在如下幾個時機:

  • 編譯時:當一個類文件被編譯時進行織入,這需要特殊的編譯器才可以做的到,例如AspectJ的織入編譯器
  • 類加載時:使用特殊的ClassLoader在目標類被加載到程序之前增強類的字節代碼
  • 運行時:切面在運行的某個時刻被織入,SpringAOP就是以這種方式織入切面的,原理應該是使用了JDK的動態代理技術

Spring對AOP的支持

創建切入點來定義切面所織入的連接點是AOP框架的基本功能。
Spring提供了4種類型的AOP支持:

  • 基于代理的經典Spring AOP
  • 純POJO切面
  • @AspectJ注解驅動的切面
  • 注入式AspectJ切面(使用與Spring各版本)

前三種都是Spring AOP實現的變體,Spring AOP構建在動態代理基礎之上,因此,Spring對AOP的支持局限于方法攔截。

這里我不準備介紹經典Spring AOP,因為引入了簡單的聲明式AOP和基于直接的AOP后,Spring經典的AOP看起來就顯得非常笨重和過于復雜。

對于新手入門來說,我們不需要知道這么多,在這里我也只介紹2,3兩種方式,簡單的說就是一個基于xml配置,一個基于注解。

下面就直接開始舉兩個例子分別來介紹下這兩種AOP方式,我們就拿簡單的日志來說明。

基于注解的方式

首先基于注解的方式需要引入這些包,對用的pom.xml如下:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>4.1.1.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjrt</artifactId>
    <version>1.8.8</version>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.8.8</version>
</dependency>

我們還是舉前面用到的UserController來說明,下面方法很簡單,執行進入這個方法的時候會打印“進來了”信息,現在我打算給這個方法加日志,在執行該方法前打印“進來前”,在執行完方法后執行“進來后”。

package com.tengj.demo.controller;

@Controller
@RequestMapping(value="/test")
public class UserController {
    @Autowired
    UserService userService;
    
    @RequestMapping(value="/view",method = RequestMethod.GET)
    public String index(){
        userService.sayHello("tengj");
        return "index";
    }
}

servie層代碼:

package com.tengj.demo.service
public interface UserService {
    public void sayHello(String name);
}

servie實現類代碼:

package com.tengj.demo.service.impl;
@Service("userService")
public class UserServiceImpl implements UserService{
    @Override
    public void sayHello(String name) {
        System.out.println("hello,"+name);
    }
}

上面方法index()其實就是我們之前定義的切點,表示在哪里切入AOP。



如圖所示,我們使用execution()指示器選擇UserServiceImpl的sayHello方法。方法表達式以“*”號開始,表明了我們不關心方法返回值的類型。然后,我們指定了全限定類名和方法名。對于方法參數列表,我們使用兩個點號(..)表明切點要選擇任意的sayHello()方法,無論該方法的入參是什么。

接下來我們要定義個切面,也就是所謂的日志功能的類。

package com.tengj.demo.aspect;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Component //注入依賴
@Aspect //該注解標示該類為切面類
public class LogAspect {
    @Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
    public void logAop(){}

    @Before("logAop() && args(name)")
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    @AfterReturning("logAop()")
    public void logAfterReturning(){
        System.out.println("返回通知AfterReturning");
    }

    @After("logAop() && args(name)")
    public void logAfter(String name){
        System.out.println(name+"后置通知After");
    }

    @AfterThrowing("logAop()")
    public void logAfterThrow(){
        System.out.println("異常通知AfterThrowing");
    }
}

上面就是切面類的代碼,很簡單,這里用到了前面提的通知的幾種類型。
這樣就能實現切入功能了

@Pointcut("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
public void logAop(){}

這里的@Pointcut注解是為了定義切面內重用的切點,也就是說把公共的東西抽出來,定義了任意的方法名稱logAop,這樣下面用到的各種類型通知就只要寫成

@Before("logAop() && args(name)")
@AfterReturning("logAop()")
@AfterThrowing("logAop()")

這樣既可,否則就要寫成

@Before("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterReturning("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")
@AfterThrowing("execution(* com.tengj.demo.service.impl.UserServiceImpl.*(..))")

大家是否注意到了@Before("logAop() && args(name)")這里多出來個&& args(name),這個是用來傳遞參數的,定義只要跟sayHello參數名稱一樣就可以。

如果就此止步的話,LogAspect只會是Spring容器中的一個Bean,即便使用了AspectJ注解,但它并不會被視為切面,這些注解不會解析,也不會創建將其轉換為切面的代理。

所以需要在XML里面配置一下,需要使用Spring aop命名空間中的<aop:aspectj-autoproxy/>元素,簡單如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
       http://www.springframework.org/schema/beans/spring-beans-4.1.xsd
       http://www.springframework.org/schema/context 
       http://www.springframework.org/schema/context/spring-context-4.1.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc-4.1.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd"
       default-lazy-init="true">
    <context:component-scan base-package="com.tengj.demo"/>
    <mvc:resources location="/WEB-INF/pages/" mapping="/pages/**"/>
    <!-- 默認的注解映射的支持 -->
    <mvc:annotation-driven/>
    <!--啟用AspectJ自動代理-->
    <aop:aspectj-autoproxy/>
    <!-- 視圖解析器 -->
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/pages/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

接著就可以啟動工程,訪問index這個方法,http://localhost:8080/SpringMVCMybatis/test/view
執行結果:

tengj前置通知Before
hello,tengj
tengj后置通知After
返回通知AfterReturning

根據前面學的我們知道,除了上面提到的通知外,還有一個更強大通知類型,就是環繞通知。可以自定義我們需要切入的位置,可以替代上面提到的所有通知。看例子:

@Around("logAop()")
public void logAround(ProceedingJoinPoint jp){
    try {
        System.out.println("自定義前置通知Before");
        jp.proceed();//將控制權交給被通知的方法,也就是執行sayHello方法
        System.out.println("自定義后置通知After");
    } catch (Throwable throwable) {
        System.out.println("異常處理~");
        throwable.printStackTrace();
    }
}

執行結果:

自定義前置通知Before
hello,tengj
自定義后置通知After

這里主要是通過ProceedingJoinPoint這個參數。其中里面的proceed()方法就是將控制權交給被通知的方法。如果你忘記調用這個方法,那么你的通知實際上會阻塞對被通知方法的調用。

有意思的是,你可以不調用proceed()方法,從而阻塞堆被通知方法的訪問,與之類似,你也可以在通知中對它進行多次調用。要這樣做的一個場景就是實現重試邏輯,也就是在被通知方法失敗后,進行重復嘗試。

基于XML配置的方式

這里介紹使用XML配置的方式來實現,在Spring的aop命名空間中,提供了多個元素用來在XML中聲明切面。

AOP配置元素 用 途
<aop:advisor> 定義AOP通知器
<aop:after> 定義AOP后置通知(不管被通知的方法是否執行成功)
<aop:after-returning> 定義AOP返回通知
<aop:after-throwing> 定義AOP異常通知
<aop:around> 定義AOP環繞通知
<aop:aspect> 定義一個切面
<aop:aspectj-autoproxy> 啟用@AspectJ注解驅動的切面
<aop:before> 定義一個AOP前置通知
<aop:config> 頂層的AOP配置元素,大多數的<aop:*>元素必須包含在<aop:config>元素內
<aop:declare-parents> 以透明的方式為被通知的對象引入額外的接口
<aop:pointcut> 定義一個切點

我們已經看過了<aop:aspectj-autoproxy/>元素,它能夠自動代理AspectJ注解的通知類。aop命名空間的其他元素能夠讓我們直接在Spring配置中聲明切面,而不需要使用注解。
所以,我們重新來看看一下這個LogAspect類,這次我們將它所有的AspectJ注解全部移除掉:

package com.tengj.demo.aspect;

public class LogAspect {
    public void logBefore(String name){
        System.out.println(name+"前置通知Before");
    }

    public void logAfterReturning(String name){
        System.out.println("返回通知AfterReturning");
    }

    public void logAfter(String name){
        System.out.println(name+"后置通知After");
    }

    public void logAfterThrow(String name){
        System.out.println("異常通知AfterThrowing");
    }
}

然后在xml配置文件中使用Spring aop命名空間中的一些元素,詳細基本配置參考上面注解方式中的xml配置,這里是貼出來關鍵的代碼:

<bean id="logAspect" class="com.tengj.demo.aspect.LogAspect" />
<aop:config>
        <aop:aspect id="log"  ref="logAspect">
            <aop:pointcut id="logAop" expression="execution(* com.tengj.demo.service.impl.UserServiceImpl.sayHello(..)) and args(name)"/>
            <aop:before method="logBefore" pointcut-ref="logAop"/>
            <aop:after method="logAfter"  pointcut-ref="logAop"/>
            <aop:after-returning method="logAfterReturning"  pointcut-ref="logAop"/>
            <aop:after-throwing method="logAfterThrow" pointcut-ref="logAop"/>
            <!--<aop:around method="logAfterThrow"  pointcut-ref="logAop"/>-->
        </aop:aspect>
</aop:config>

配置也 很好理解

  • xml里面配置aop,都是放在<aop:config>里面
  • 然后使用<aop:aspect>一個切面,指向具體的bean類。
  • 使用<aop:pointcut>定義切點,基本跟注解的很像,其中要注意的是xml配置里面如果要帶參數的,用的不再是&&,要使用and關鍵字才行(因為在XML中,“&”符號會被解析為實體的開始)
  • 然后就是使用各種通知標簽了,簡單。

執行效果如下:

tengj前置通知Before
hello,tengj
tengj后置通知After
返回通知AfterReturning

環繞通知也很簡單,直接貼代碼:
xml配置:

<aop:around method="logAround"  pointcut-ref="logAop"/>

切面方法:

public void logAround(ProceedingJoinPoint jp,String name){
    try {
        System.out.println(name+"自定義前置通知Before");
        jp.proceed();
        System.out.println(name+"自定義后置通知After");
    } catch (Throwable throwable) {
        System.out.println("異常處理~");
        throwable.printStackTrace();
    }
}

執行結果:

tengj自定義前置通知Before
hello,tengj
tengj自定義后置通知After

總結

Spring AOP是Spring學習中最關鍵的,我總結的這2種寫法也是開發中最常用的。也不知道大家能不能理解~看得時候如果有不懂的地方可以提出來,我好修改一下,讓更多的人理解并掌握AOP,希望對你有所幫助。


一直覺得自己寫的不是技術,而是情懷,一篇篇文章是自己這一路走來的痕跡。靠專業技能的成功是最具可復制性的,希望我的這條路能讓你少走彎路,希望我能幫你抹去知識的蒙塵,希望我能幫你理清知識的脈絡,希望未來技術之巔上有你也有我。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,646評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,595評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,560評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,035評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,814評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,224評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,301評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,444評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,988評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,804評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,998評論 1 370
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,544評論 5 360
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,237評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,665評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,927評論 1 287
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,706評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,993評論 2 374

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,785評論 18 139
  • 本章內容: 面向切面編程的基本原理 通過POJO創建切面 使用@AspectJ注解 為AspectJ切面注入依賴 ...
    謝隨安閱讀 3,176評論 0 9
  • AOP實現可分為兩類(按AOP框架修改源代碼的時機): 靜態AOP實現:AOP框架在編譯階段對程序進行修改,即實現...
    數獨題閱讀 2,330評論 0 22
  • 以我的觀念來看,人總是有顆爭強好勝的心,或多或少,時強時弱。 當你的付出與回報不成正比的時候,當你求而不得...
    Tawnie涵閱讀 261評論 0 0