SpringBoot2之AOP切面程式設計
Why AOP?
Aspect Oriented Programming(AOP),面向切面程式設計,是一個比較熱門的話題。AOP主要實現的目的是針對業務處理過程中的切面進行提取,它所面對的是處理過程中的某個步驟或階段,以獲得邏輯過程中各部分之間低耦合性的隔離效果。比如我們最常見的就是日誌記錄了,舉個例子,我們現在提供一個查詢學生資訊的服務,但是我們希望記錄有誰進行了這個查詢。如果按照傳統的OOP的實現的話,那我們實現了一個查詢學生資訊的服務介面(StudentInfoService)和其實現 類 (StudentInfoServiceImpl.java),同時為了要進行記錄的話,那我們在實現類(StudentInfoServiceImpl.java)中要新增其實現記錄的過程。這樣的話,假如我們要實現的服務有多個呢?那就要在每個實現的類都新增這些記錄過程。這樣做的話就會有點繁瑣,而且每個實現類都與記錄服務日誌的行為緊耦合,違反了面向物件的規則。那麼怎樣才能把記錄服務的行為與業務處理過程中分離出來呢?看起來好像就是查詢學生的服務自己在進行,但卻是背後日誌記錄對這些行為進行記錄,並且查詢學生的服務不知道存在這些記錄過程,這就是我們要討論AOP的目的所在。AOP的程式設計,好像就是把我們在某個方面的功能提出來與一批物件進行隔離,這樣與一批物件之間降低了耦合性,可以就某個功能進行程式設計。
AOP的幾個概念
-
切面(Aspect):一個關注點的模組化,這個關注點可能會橫切多個物件。事務管理是J2EE應用中一個關於橫切關注點的很好的例子。在Spring AOP中,切面可以使用基於模式或者基於@Aspect註解的方式來實現。
-
連線點(Joinpoint):在程式執行過程中某個特定的點,比如某方法呼叫的時候或者處理異常的時候。在Spring AOP中,一個連線點總是表示一個方法的執行。
-
通知(Advice):在切面的某個特定的連線點上執行的動作。其中包括了“around”、“before”和“after”等不同型別的通知(通知的型別將在後面部分進行討論)。許多AOP框架(包括Spring)都是以攔截器做通知模型,並維護一個以連線點為中心的攔截器鏈。
-
切入點(Pointcut):匹配連線點的斷言。通知和一個切入點表示式關聯,並在滿足這個切入點的連線點上執行(例如,當執行某個特定名稱的方法時)。切入點表示式如何和連線點匹配是AOP的核心:Spring預設使用AspectJ切入點語法。
-
引入(Introduction):用來給一個型別宣告額外的方法或屬性(也被稱為連線型別宣告(inter-type declaration))。Spring允許引入新的介面(以及一個對應的實現)到任何被代理的物件。例如,你可以使用引入來使一個bean實現IsModified介面,以便簡化快取機制。
-
目標物件(Target Object):被一個或者多個切面所通知的物件。也被稱做被通知(advised)物件。既然Spring AOP是通過執行時代理實現的,這個物件永遠是一個被代理(proxied)物件。
-
AOP代理(AOP Proxy):AOP框架建立的物件,用來實現切面契約(例如通知方法執行等等)。在Spring中,AOP代理可以是JDK動態代理或者CGLIB代理。
-
織入(Weaving):把切面連線到其它的應用程式型別或者物件上,並建立一個被通知的物件。這些可以在編譯時(例如使用AspectJ編譯器),類載入時和執行時完成。Spring和其他純Java AOP框架一樣,在執行時完成織入。
Maven引入AOP
再SpringBoot2中只需要引入aop-starter即可使用AOP切面程式設計
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
效果展示
開源專案
以下專案和程式碼可以在我的開源專案spring-cloud-study中的spring-boot-study-aop
中GET到。
https://github.com/moshowgame/spring-cloud-study
DEMO程式碼
AOP切面主方法切面如下:
package com.softdev.system.demo.config;
import com.alibaba.fastjson.JSON;
import lombok.extern.java.Log;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;
@Aspect
@Component
@Log
public class AspectConfig {
@Pointcut("execution(public * com.softdev.system.demo.controller.DemoController.index(..))")
public void index_log(){}
/**
* 記錄HTTP請求結束時的日誌
*/
@Before("index_log()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 接收到請求,記錄請求內容
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 記錄下請求內容
log.info(">>>>>>>>>>Before");
log.info("URL : " + request.getRequestURL().toString());
log.info("HTTP_METHOD : " + request.getMethod());
log.info("IP : " + request.getRemoteAddr());
log.info("PATH : " + request.getServletPath());
log.info("METHOD : " + request.getMethod());
log.info("CLASS_METHOD : " + joinPoint.getSignature().getDeclaringTypeName() + "." + joinPoint.getSignature().getName());
log.info("ARGS : " + Arrays.toString(joinPoint.getArgs()));
}
@AfterReturning(returning = "obj",pointcut = "index_log()")
public void doAfterReturning(Object obj) throws Throwable {
//處理完請求,返回內容
log.info(">>>>>>>>>>AfterReturning");
log.info("RESPONSE : " + JSON.toJSONString(obj));
}
@AfterThrowing(value = "index_log()",throwing = "exception")
public void doAfterThrowing(JoinPoint joinPoint,Throwable exception){
//目標方法名:
log.info(">>>>>>>>>>AfterThrowing");
log.info(joinPoint.getSignature().getName());
if(exception instanceof NullPointerException){
log.info("發生了空指標異常!!!!!");
}else{
log.info("發生了未知異常!!!!!");
}
}
@Around(value = "index_log()")
public Object doAroundAdvice(ProceedingJoinPoint proceedingJoinPoint){
log.info(">>>>>>>>>>Around");
log.info("環繞通知的目標方法名:"+proceedingJoinPoint.getSignature().getName());
try {
Object obj = proceedingJoinPoint.proceed();
return obj;
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return null;
}
}
Controller方法如下
@RestController
public class DemoController {
@GetMapping("/index")
public ApiReturnObject index(String data){
if(StringUtils.isEmpty(data)) {
data="hello spring-cloud-study";
}
return ApiReturnUtil.success(data);
}
}
@AfterReturning後置返回通知
在某連線點之後執行的通知,通常在一個匹配的方法返回的時候執行(可以在後置通知中繫結返回值)。
- 如果引數中的第一個引數為
JoinPoint
,則第二個引數為返回值
的資訊。 - AfterReturning限定了只有目標方法返回值與通知方法
相同型別
的引數時才能執行後置返回通知,否則不執行。 - 對於returning對應的通知方法引數為
Object
型別將匹配任何目標返回值。
@AfterThrowing後置異常通知
在方法丟擲異常退出時執行的通知
@After後置最終通知
當某連線點退出時執行的通知(不論是正常返回還是異常退出)。
@Around環繞通知
包圍一個連線點的通知,如方法呼叫等。這是最強大
也最麻煩
的一種通知型別,可以在方法呼叫前後完成自定義
的行為,它也會選擇是否繼續執行連線點或者直接返回它自己的返回值或丟擲異常來結束執行。
對方法的環繞,具體方法會通過代理傳遞到切面中去,切面中可選擇執行方法與否,執行幾次方法等。環繞通知使用一個代理ProceedingJoinPoint
型別的物件來管理目標物件,所以此通知的第一個引數必須是ProceedingJoinPoint型別
。在通知體內呼叫ProceedingJoinPoint的proceed()方法
會導致後臺的連線點方法執行。proceed()
方法也可能會被呼叫並且傳入一個Object[]
物件,該陣列中的值將被作為方法執行時的入參。
切入點表示式
定義切入點的時候需要一個包含名字和任意引數的簽名,還有一個切入點表示式,如@Pointcut("execution(public * com.softdev.system.demo.controller.DemoController.index(..))")
意思是,指定了DemoController的index方法。
切入點表示式的格式為
execution([可見性]返回型別[宣告型別].方法名(引數)[異常])
其中[]內的是可選的,其它的還支援萬用字元的使用:
*
:匹配所有字元..
:一般用於匹配多個包,多個引數+
:表示類及其子類- 運算子有:
&&
,||
,!
切入點指示符
-
execution - 匹配方法執行的連線點,這是你將會用到的Spring的最主要的切入點指示符。
-
within - 限定匹配特定型別的連線點(在使用Spring AOP的時候,在匹配的型別中定義的方法的執行)。
-
this - 限定匹配特定的連線點(使用Spring AOP的時候方法的執行),其中bean reference(Spring AOP 代理)是指定型別的例項。
-
target - 限定匹配特定的連線點(使用Spring AOP的時候方法的執行),其中目標物件(被代理的應用物件)是指定型別的例項。
-
args - 限定匹配特定的連線點(使用Spring AOP的時候方法的執行),其中引數是指定型別的例項。
-
@target - 限定匹配特定的連線點(使用Spring AOP的時候方法的執行),其中正執行物件的類持有指定型別的註解。
-
@args - 限定匹配特定的連線點(使用Spring AOP的時候方法的執行),其中實際傳入引數的執行時型別持有指定型別的註解。
-
@within - 限定匹配特定的連線點,其中連線點所在型別已指定註解(在使用Spring AOP的時候,所執行的方法所在型別已指定註解)。
-
@annotation - 限定匹配特定的連線點(使用Spring AOP的時候方法的執行),其中連線點的主題持有指定的註解。
當然,其中execution使用最頻繁也是最方便,即某方法執行時進行切入。