文章目錄
- 一、AOP簡介
- 二、AOP體系與概念
- 三、AOP實例
- 1、創建SpringBoot工程
- 2、添加依賴
- 3、AOP相關注解
- 3.1、@Aspect
- 3.2、@Pointcut
- 3.2.1、execution()
- 3.2.2、annotation()
- 3.3、@Around
- 3.4、@Before
- 3.5、@After
- 3.6、@AfterReturning
- 3.7、@AfterThrowing
一、AOP簡介
AOP(Aspect Oriented Programming)
,面向切面思想,是Spring的三大核心思想之一(其余兩個:IOC - 控制反轉
、DI - 依賴注入
)。
那么AOP為何那么重要呢?
在我們的程序中,經常存在一些系統性的需求,比如 權限校驗
、日志記錄
、統計
等,這些代碼會散落穿插在各個業務邏輯中,非常冗余且不利于維護,那么面向切面編程往往讓我們的開發更加低耦合,也大大減少了代碼量,同時呢讓我們更專注于業務模塊的開發,把那些與業務無關的東西提取出去,便于后期的維護和迭代。
二、AOP體系與概念
簡單地去理解,其實AOP要做三類事:
-
在哪里切入
,也就是權限校驗等非業務操作在哪些業務代碼中執行。 -
在什么時候切入
,是業務代碼執行前還是執行后。 -
切入后做什么事
,比如做權限校驗、日志記錄等。
AOP的體系圖:
一些概念:
概念 | 說明 |
---|---|
Pointcut | 切點,決定處理如權限校驗、日志記錄等在何處切入業務代碼中(即織入切面)。切點分為execution 方式和 annotation 方式。前者可以用路徑表達式指定哪些類織入切面,后者可以指定被哪些注解修飾的代碼織入切面。 |
Advice | 處理,包括處理時機和處理內容。處理內容就是要做什么事,比如校驗權限和記錄日志。處理時機就是在什么時機執行處理內容,分為前置處理(即業務代碼執行前)、后置處理(業務代碼執行后)等。 |
Aspect | 切面,即 Pointcut 和 Advice 。 |
Joint point | 連接點,是程序執行的一個點。例如,一個方法的執行或者一個異常的處理。在 Spring AOP 中,一個連接點總是代表一個方法執行。 |
Weaving | 織入,就是通過動態代理,在目標對象方法中執行處理內容的過程。 |
三、AOP實例
1、創建SpringBoot工程
如何創建詳見:IDEA 創建 SpringBoot 項目
2、添加依賴
<!-- springboot-aop包,AOP切面注解,Aspectd等相關注解 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId>
</dependency>
3、AOP相關注解
package com.cw.tsb.app.aspect;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;@Component
@Aspect
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}@Around("pointCut()")public Object doAround(ProceedingJoinPoint joinPoint) {System.out.println("------------- doAround.");Object obj = null;try {obj = joinPoint.proceed();} catch (Throwable t){t.printStackTrace();}return obj;}@After("pointCut()")public void doAfter(JoinPoint joinPoint){System.out.println("------------- doAfter.");}@Before("pointCut()")public void doBefore(JoinPoint joinPoint){System.out.println("------------- doBefore.");}/*** 后置返回* 如果第一個參數為JoinPoint,則第二個參數為返回值的信息* 如果第一個參數不為JoinPoint,則第一個參數為returning中對應的參數* returning:限定了只有目標方法返回值與通知方法參數類型匹配時才能執行后置返回通知,否則不執行,* 參數為Object類型將匹配任何目標返回值*/@AfterReturning(value = "pointCut()", returning = "result")public void doAfterReturning(JoinPoint joinPoint, String result){System.out.println("doAfterReturning result = " + result);}@AfterThrowing(value = "pointCut()", throwing = "t")public void doAfterThrowing(JoinPoint joinPoint, Throwable t){System.out.println("------------- doAfterThrowing throwable = " + t.toString());}
}
3.1、@Aspect
該注解要添加在類上,聲明這是一個切面類,使用時需要與@Component注解一起用,表明同時將該類交給spring管理。
@Component
@Aspect
public class ControllerAspect {
}
3.2、@Pointcut
用來定義一個切點,即上文中所關注的某件事情的入口,切入點定義了事件觸發時機。
該注解需要添加在方法上,該方法簽名必須是 public void
類型,可以將@Pointcut
中的方法看作是一個用來引用的助記符,因為表達式不直觀,因此我們可以通過方法簽名的方式為此表達式命名。因此 @Pointcut 中的方法只需要方法簽名,而不需要在方法體內編寫實際代碼
。
該注解有兩個常用的表達式:execution()
和 annotation()
。
3.2.1、execution()
@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}
}
表達式為:
execution(* com.cw.tsb.app.controller..*.*(..))
-
第一個 *
:表示返回值類型,*
表示所有類型; -
包名
:標識需要攔截的包名; -
包名后的 ..
:表示當前包和當前包的所有子包,在本例中指com.cw.tsb.app.controller
包、子包下所有類; -
第二個 *
:表示類名,*
表示所有類; -
最后的 *(..)
:星號表示方法名,* 表示所有的方法,后面括弧里面表示方法的參數,兩個句點表示任何參數。
3.2.2、annotation()
annotation()
方式是針對某個注解來定義切點,比如我們對具有 @PostMapping
注解的方法做切面,可以如下定義切面:
@Aspect
@Component
public class ControllerAspect {@Pointcut("@annotation(org.springframework.web.bind.annotation.PostMapping)")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}
}
然后使用該切面的話,就會切入注解是 @PostMapping
的所有方法。這種方式很適合處理 @GetMapping
、@PostMapping
、@DeleteMapping
不同注解有各種特定處理邏輯的場景。
還有就是如上面案例所示,針對自定義注解來定義切面。
@Aspect
@Component
public class ControllerAspect {@Pointcut("@annotation(com.cw.tsb.app.annotation.PermissionsAnnotation)")private void permissionCheck() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}
}
3.3、@Around
@Around
注解用于修飾 Around
增強處理,Around
增強處理非常強大,表現在:
-
@Around
可以自由選擇增強動作與目標方法的執行順序,也就是說可以在增強動作前后,甚至過程中執行目標方法。這個特性的實現在于,調用ProceedingJoinPoint
參數的procedd()
方法才會執行目標方法。 -
@Around
可以改變執行目標方法的參數值,也可以改變執行目標方法之后的返回值。
Around
增強處理有以下特點:
-
當定義一個
Around
增強處理方法時,該方法的第一個形參必須是ProceedingJoinPoint
類型(至少一個形參)。在增強處理方法體內,調用ProceedingJoinPoint
的proceed
方法才會執行目標方法:這就是@Around
增強處理可以完全控制目標方法執行時機、如何執行的關鍵;如果程序沒有調用ProceedingJoinPoint
的proceed
方法,則目標方法不會執行。 -
調用
ProceedingJoinPoint
的proceed
方法時,還可以傳入一個Object[]
對象,該數組中的值將被傳入目標方法作為實參 —— 這就是Around
增強處理方法可以改變目標方法參數值的關鍵。這就是如果傳入的Object[]
數組長度與目標方法所需要的參數個數不相等,或者Object[]
數組元素與目標方法所需參數的類型不匹配,程序就會出現異常。
@Around
功能雖然強大,但通常需要在線程安全的環境下使用。因此,如果使用普通的@Before
、@AfterReturning
就能解決的問題,就沒有必要使用 Around
了。如果需要目標方法執行之前和之后共享某種狀態數據,則應該考慮使用 Around
。尤其是需要使用增強處理阻止目標的執行,或需要改變目標方法的返回值時,則只能使用 Around
增強處理了。
3.4、@Before
@Before
注解指定的方法在切面切入目標方法之前執行,可以做一些 Log
處理,也可以做一些信息的統計,比如 獲取用戶的請求 URL
以及 用戶的 IP
地址等等,這個在做個人站點的時候都能用得到,都是常用的方法。例如下面代碼:
@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}/*** 在上面定義的切面方法之前執行該方法* @param joinPoint jointPoint*/@Before("pointCut()")public void doBefore(JoinPoint joinPoint) {// 獲取簽名Signature signature = joinPoint.getSignature();// 獲取切入的包名String declaringTypeName = signature.getDeclaringTypeName();// 獲取即將執行的方法名String funcName = signature.getName();log.info("即將執行方法為: {},屬于{}包", funcName, declaringTypeName);// 也可以用來記錄一些信息,比如獲取請求的 URL 和 IPServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();// 獲取請求 URLString url = request.getRequestURL().toString();// 獲取請求 IPString ip = request.getRemoteAddr();}
}
JointPoint
對象很有用,可以用它來獲取一個簽名,利用簽名可以獲取請求的包名、方法名,包括參數(通過 joinPoint.getArgs()
獲取)等。
3.5、@After
@After
注解和 @Before
注解相對應,指定的方法在切面切入目標方法之后執行,也可以做一些完成某方法之后的 Log
處理。
@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}/*** 在上面定義的切面方法之后執行該方法* @param joinPoint jointPoint*/@After("pointCut()")public void doAfter(JoinPoint joinPoint) {log.info("==== doAfter 方法進入了====");Signature signature = joinPoint.getSignature();String method = signature.getName();log.info("方法{}已經執行完", method);}
}
到這里,我們來寫個 Controller
測試一下執行結果,新建一個 AopController
如下:
@RestController
@RequestMapping("/aop")
public class AopController {@GetMapping("/{name}")public String testAop(@PathVariable String name) {return "Hello " + name;}
}
啟動項目,在瀏覽器中輸入:http://localhost:8080/aop/csdn,觀察一下控制臺的輸出信息:
====doBefore 方法進入了====
即將執行方法為: testAop,屬于com.itcodai.mutest.AopController包
用戶請求的 url 為:http://localhost:8080/aop/name,ip地址為:0:0:0:0:0:0:0:1
==== doAfter 方法進入了====
方法 testAop 已經執行完
從打印出來的 Log
中可以看出程序執行的邏輯與順序,可以很直觀的掌握 @Before
和 @After
兩個注解的實際作用。
3.6、@AfterReturning
@AfterReturning
注解和 @After
有些類似,區別在于 @AfterReturning
注解可以用來捕獲切入方法執行完之后的返回值,對返回值進行業務邏輯上的增強處理,例如:
@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}/*** 后置返回* 如果第一個參數為JoinPoint,則第二個參數為返回值的信息* 如果第一個參數不為JoinPoint,則第一個參數為returning中對應的參數* returning:限定了只有目標方法返回值與通知方法參數類型匹配時才能執行后置返回通知,否則不執行,* 參數為Object類型將匹配任何目標返回值*/@AfterReturning(value = "pointCut()", returning = "result")public void doAfterReturning(JoinPoint joinPoint, String result){// 實際項目中可以根據業務做具體的返回值增強}
}
需要注意的是,在 @AfterReturning
注解 中,屬性 returning
的值必須要和參數保持一致,否則會檢測不到。該方法中的第二個入參就是被切方法的返回值,在 doAfterReturning
方法中可以對返回值進行增強,可以根據業務需要做相應的封裝。
3.7、@AfterThrowing
當被切方法執行過程中拋出異常時,會進入 @AfterThrowing
注解的方法中執行,在該方法中可以做一些異常的處理邏輯。要注意的是 throwing
屬性的值必須要和參數一致,否則會報錯。該方法中的第二個入參即為拋出的異常。
@Aspect
@Component
public class ControllerAspect {@Pointcut("execution(* com.cw.tsb.app.controller..*.*(..))")public void pointCut() {//該方法僅用于掃描controller包下類中的方法,而不做任何特殊的處理。}@AfterThrowing(value = "pointCut()", throwing = "t")public void doAfterThrowing(JoinPoint joinPoint, Throwable t){System.out.println("------------- doAfterThrowing throwable = " + t.toString());// 處理異常的邏輯}
}