淺談Spring AOP 面向切面編程 最通俗易懂的畫圖理解AOP、AOP通知執行順序~
- 2019 年 11 月 10 日
- 筆記
簡介
我們都知道,Spring 框架作為後端主流框架之一,最有特點的三部分就是IOC控制反轉、依賴注入、以及AOP切面。當然AOP作為一個Spring
的重要組成模組,當然IOC是不依賴於Spring框架的,這就說明你有權選擇是否要用AOP來完成一些業務。
AOP面向切面編程,通過另一種思考的方式,來彌補面向對象編程OOP當中的不足,OOP當中最重要的單元是類,所以萬物皆對象,萬物皆是
對象類。而在AOP的模組單元中,最基礎的單元是切面,切面對切點進行模組化的管理。
最後再提一句:Spring當中的AOP是利用Java的代理模式實現的
AOP概念
讓我們從一些基礎的術語開始了解面向切面編程AOP,術語不是特別的直觀,最好的方式就是通過文本理解+影像理解+程式碼實例理解
這樣對於我們來說才是真正意義上的理解。
- 切面:(Aspect) 一個關注點的模組化,就比較籠統的一個概念,關注點可能橫切多個對象。若不理解請往後看圖片理解,對應的註解有@Aspect。
- 連接點:(Joinpoint) 在程式執行過程中某個特定的點,一個連接點總是代表一個方法的執行。
- 通知:(Advice) 通知表示在一個連接點執行的具體的動作,比如After Before 表明通知的具體動作
- 切入點:(Pointcut)通過一個表達式去表明我所定義的通知在那個地點具體執行。
- 前置通知:(Before advice)表明在連接點執行之前執行的動作。
- 後置通知:(After returning advice)在某個連接點完成後的通知,比如一個方法沒有拋出任何異常,正常返回。
- 環繞通知:(Around Advice) 環繞可以看作是包含前置通知和後置通知的一個通知,先了解,後面具體理解。
- 異常通知:(After throwing advice) 在方法異常推出時候執行的通知。
- 最終通知:(After advice) 在連接點退出時候執行的通知。不論是正常退出還是異常退出。
說了這麼多,都感覺迷迷糊糊的,我們首先來看一個例子吧,通過這個例子來理解AOP切面,通過例子在具體說明
Springboot AOP
版本資訊:Springboot 2.1.6
添加依賴 Web AOP
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
寫一個最簡單的控制器
@RestController @RequestMapping("/") public class AuthController { private Logger logger = LoggerFactory.getLogger(AuthController.class); @GetMapping("login") public String login(String user,String pass) { logger.info("login--->user",user); logger.info("login--->pass",pass); return "success"; } }
這樣的控制器在SpringBoot的Web開發當中是很常見的,收到前端的請求,將參數進行校驗,我們這裡為了簡單,不作校驗,只是列印出來,為了配合我們後面的AOP的理解而
做的一個最簡單的控制器,測試請求以下路徑,控制台列印以下內容
http://localhost:8080/login?user=root&pass=root
2019-11-09 09:59:00.585 INFO 11100 --- [nio-8080-exec-1] c.e.demo.controller.AuthController : login--->user 2019-11-09 09:59:00.588 INFO 11100 --- [nio-8080-exec-1] c.e.demo.controller.AuthController : login--->pass
定義切面類
1、定義一個切面類,加入@Aspect註解和@Component註解
@Aspect @Component public class WebLogAsp {}
@Aspect 註解將找個類定義為一個切面對象,通過@Component註解將這個類對象注入到IOC容器,交給Spring來進行管理。
2、定義一個切入點 通過@Pointcut
@Pointcut("execution(public * com.example.demo.controller.*.*(..))") public void controllerLog(){}
@Pointcut這個註解主要用來定義切入點,通過表達式的方式,來告訴Spring,我這個切點要切到什麼位置,常用的就是execution去匹配連接點。
主要來說一下execution 匹配表達是的表達方法,我們按照以下的例子來說明:
@Pointcut("execution(public * com.example.demo.controller.*.*(..))")
語法:execution( [方法修飾符(可選)]__返回類型__類路徑__方法名__(參數)__[異常模式(可選)] )
這裡我用下劃線來代替空格,比較直觀的可以看出,我們這個例子裡面,我將這個切點切入到com.example.demo.controller包下所有類的所有方法上面。
*就是通配符,(..)代表任意多個參數,也就說明我切入到的方法它的參數我是不限定的,可以有任意個參數。
3、定義通知項,@Before定義一個前置通知
@Before("controllerLog()") public void beforeAdvice(JoinPoint joinPoint){}
@Before註解傳入字元串方法名,也就是我們上面定義的切入點的方法名,告訴它這個通知是在切入點 controllerLog()上面執行的通知,它會在切入點方法執行之前率先執行
這個方法傳入了一個JoinPoint 對象,也就是我們所說的連接點對象,連接點可以理解為切入點方法執行時候所產生的一個對象。
這裡有幾個具體的方法很重要,需要說明一下:
-
Object[] getArgs();獲取切入點方法執行時候傳入的參數。
-
Object getTarget();獲取的是切入方法所在的類對象,簡單理解例子裡面的切入點是login()方法,所以返回的對象就是AuthController對象
-
Object getThis();返回AOP框架為目標對象生成的代理對象。
這裡將詳細程式碼舉例,通過列印的方式進行輸出,通過SpringMvc的RequestContextHolder獲取執行緒的請求對象,這裡我們可以拿到當前請求
的一些具體參數,比如訪問人的IP資訊,它所請求的URL以及請求所帶的參數等待。
具體請參考:https://www.runoob.com/servlet/servlet-client-request.html
@Before("controllerLog()") public void beforeAdvice(JoinPoint joinPoint){ logger.info("前置通知開始--->"); /*獲取當前請求的HttpServletRequest*/ RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); HttpServletRequest request = ((ServletRequestAttributes)requestAttributes).getRequest(); logger.info("URL-->"+request.getRequestURL().toString()); logger.info("IP-->"+request.getRemoteAddr()); logger.info("HTTP_Method-->"+request.getMethod()); logger.info("Request_args-->"+ Arrays.toString(joinPoint.getArgs())); logger.info("前置通知結束--->"); }
4、環繞通知方法 @Around
首先來看一部分程式碼,這裡傳遞了一個 ProceedingJoinPoint 對象,它是JoinPoint 切點對象的一個子類,也可以為它是切點所在的方法執行時候所產生的一個對象
這裡最重要的一個方法當屬於 proceed() ;執行proceed()就表示執行切點所在的方法執行,它會返回一個Object對象,也就是目標方法所返回的對象。
@Around("controllerLog()") public Object aroundAdvice(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { logger.info("環繞通知開始-->"); Object result = null; /*proceed()方法表示連接點方法執行 result 為連接點方法的返回值*/ result = proceedingJoinPoint.proceed(); logger.info("環繞通知返回值-->" + result); logger.info("環繞通知結束-->"); return result; }
踩坑:環繞方法一定要將result對象返回出去,若定義為void 無返回值的話,將在後面的@AfterReturning 後置通知中無法取到值!!!!
5、後置通知 @AfterReturning 連接點正常執行完畢後的通知
@AfterReturning(value = "controllerLog()",returning = "obj") public void afterReturning(JoinPoint joinPoint,Object obj){ logger.info("後置通知正常返回-->"+obj); }
這裡傳遞了兩個參數,第一個參數還是指定切點,第二個參數指定的是返回值,這個返回值就是連接點所返回的參數值,需要在環繞通知裡面將其返回出來才可以
取到值,不然返回的就是Null
6、異常通知 @AfterThrowing
@AfterThrowing(value = "controllerLog()", throwing = "ex") public void afterThrowing(JoinPoint joinPoint,Exception ex){ logger.info("異常通知-->"+ex.getMessage()); }
這裡和上面的後置通知差不多,不過第二個參數是一個異常類型對象,可以取出發生異常時候的異常資訊。
7、最終通知 @After
這個和@Before一樣,不再細說!
正常返回測試
這裡可以很明確的看到,一個正常的返迴路徑,相信我不說大家都可以看的清楚,畫圖最清楚的,接收到請求後,首先工作的是環繞通知,
環繞通知裡面執行proceed()方法後,才會進入連接點方法執行,Before是在連接點方法執行之前所執行的,而後執行連接點方法內容,
正常執行完後,不發生異常情況,環繞通知結束,就會執行最終通知After After執行完畢後,才會執行後置通知AfterReturning
異常情況測試返回
我們在控制器裡面模擬進行一個異常的拋出,看一下執行的順序
logger.info("將要拋出異常"); int a = 1/0;
可以很明明顯的看到,我們在控制器裡面通過一個除0的操作,進行拋出一個異常,這裡環繞通知是沒有執行完畢的,因為拋出異常,停止了運行
從接收到請求開始,進入環繞通知,然後環繞通知裡面調用了proceed()方法,其實就是讓連接點的方法開始運行,這時候前置通知首先跑起來,可以看到
前置通知是完完全全走完的。前置通知完畢後,下來是連接點的方法運行起來了,這裡因為拋出了異常,沒有進行捕獲,最終通知還是一樣正常執行,不過最後執行的是異常的通知,
而不是像上面一樣。這裡不同的地方還是要多進行理解。
畫圖理解
1、理解切面、切入點、連接點、通知、目標對象
2、理解通知點正常與異常執行順序
小結
通過對SpringAOP框架的研究,以及畫圖的理解,能夠更深刻的理解裡面運行時候的內層含義,以及AOP的一些應用,比如系統裡面的日誌系統,通過學習AOP
完完全全可以很輕鬆的通過切面,將需要記錄的日誌資訊存到資料庫, 節約大量時間。
參考:
https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/aop-api.html
http://shouce.jb51.net/spring/aop.html
https://www.cnblogs.com/wangshen31/p/9379197.html
程式碼:
https://gitee.com/mrc1999/Springboot-aop