再來一片文章實戰 AOP
技術標籤:開發經驗
因為要寫一個需求,就是統計介面的訪問次數,還希望這些介面的訪問時間,以及是否訪問成功。
所有我簡單得的設計了一下,不想引入更多的技術,所以我選擇列印日誌的方式(主流的方式是使用ELK,這裡不展開了),並且把日誌列印到檔案,用於分析。
在網上看了蠻多文章,感覺都很亂,所以自己整理一下。我不會大幅度的介紹AOP是什麼,以及裡邊的概念有什麼,一切以栗子,需求為主導來展開。最後給一個完整的例子,前邊我會分析,什麼時候用到了AOP中的哪些概念。
想要在springboot中使用AOP,在POM中引入依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
明白我想對什麼進行處理,或者說增強
舉個栗子,我想對controller進行攔擊。因為我想知道這個介面被呼叫了多少次。
所以現在明確了,我關注的點(切入點)應該是controller包下的所有的方法。
如何告訴框架,我關注的是這些方法呢?
定義一個AOP的類如下,裡邊的@Pointcut("within(com.angus.controller..*)"),就是說,我要切controller包中所有的方法
/** * @author Gjing **/ @Aspect @Component public class TestAspect { @Pointcut("within(com.angus.controller..*)") public void testCut() { } }
除了上邊的栗子,我們往往會有不一樣的需求,比方說,我只對某個方法。有耐心的話,就看看下邊的更多的切入想要的方法的技巧。切入的點的方式有五種:
1、execution表示式
用於匹配方法執行的連線點,屬於方法級別
語法:execution(修飾符 返回值型別 方法名(引數)異常)
語法引數 | 描述 |
---|---|
修飾符 | 可選,如public,protected,寫在返回值前,任意修飾符填* 號就可以 |
返回值型別 | 必選 ,可以使用* 來代表任意返回值 |
方法名 | 必選 ,可以用* 來代表任意方法 |
引數 | () 代表是沒有引數,(..) 代表是匹配任意數量,任意型別的引數,當然也可以指定型別的引數進行匹配,如要接受一個String型別的引數,則(java.lang.String) (java.lang.String..) 等等。。。 |
異常 | 可選,語法:throws 異常 ,異常是完整帶包名,可以是多個,用逗號分隔 |
符號:
符號 | 描述 |
---|---|
* | 匹配任意字元 |
.. | 匹配多個包或者多個引數 |
+ | 表示類及其子類 |
條件符:
符號 | 描述 |
---|---|
&&、and | 與 |
|| | 或 |
! | 非 |
案例:
- 攔截com.gj.web包下的所有子包裡的任意類的任意方法
execution(* com.gj.web..*.*(..))
- 攔截com.gj.web.api.Test2Controller下的任意方法
execution(* com.gj.web.api.Test2Controller.*(..))
- 攔截任何修飾符為public的方法
execution(public * * (..))
- 攔截com.gj.web下的所有子包裡的以ok開頭的方法
execution(* com.gj.web..*.ok*(..))
更多用法大家可以根據語法自行設計,本文不在進行舉例
2、@annotation
根據所應用的註解對方法進行過濾
語法:@annotation(註解全路徑)
例項:
對用了com.gj.annotations.Test註解的所有方法進行攔截@annotation(com.gj.annotations.Test)
3、Within
根據型別(比如介面、類名或者包名過濾方法)進行攔截
語法:within(typeName)
示例:
- 對com.gj.web下的所有子包的所有方法進行攔截
within(com.gj.web..*)
更多用法可以根據語法自行設計
4、@Within
用於匹配所有持有指定註解型別內的方法,與within是有區別的,within是用於匹配指定型別內的方法執行,而@within是指定註解型別內的方法
5、bean
Spring AOP擴充套件的,AspectJ沒有對於它的指示符,用於匹配特定名稱的bean物件的執行方法
切面通知
在上邊的程式碼中,其實就包含了切面。然後@Before@After@AfterReturn@AfterThrowing@Around這些定義的,其實就是我要做什麼操作。
註解 | 描述 |
---|---|
@Before | 前置通知, 在方法執行之前執行 |
@After | 後置通知, 在方法執行之後執行,比如要在呼叫完成以後,記錄日誌 |
@AfterReturn | 返回通知, 在方法返回結果之後執行,比如要在呼叫完成以後,記錄日誌。我覺得這個更好一點,對於記錄日誌。這個是返回結果後記錄日誌的,花費的時間不會到介面響應中。 |
@AfterThrowing | 異常通知, 在方法丟擲異常之後 ,這裡比如要事務回滾 |
@Around | 環繞通知,圍繞方法的執行 ,比方要統計介面的處理時間 |
定義增強方法的通知,如下:
/**
* @author Gjing
**/
@Aspect
@Component
public class TestAspect {
@Pointcut("within(com.angus.controller..*)")
public void testCut() {
}
@Before("testCut()")
public void cutProcess(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP開始攔截, 當前攔截的方法名: " + method.getName());
}
@After("testCut()")
public void after(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP執行的方法 :"+method.getName()+" 執行完了");
}
@Around("testCut()")
public Object testCutAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("註解方式AOP攔截開始進入環繞通知.......");
Object proceed = joinPoint.proceed();
System.out.println("準備退出環繞......");
return proceed;
}
/**
* returning屬性指定連線點方法返回的結果放置在result變數中
* @param joinPoint 連線點
* @param result 返回結果
*/
@AfterReturning(value = "testCut()",returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP攔截的方法執行成功, 進入返回通知攔截, 方法名為: "+method.getName()+", 返回結果為: "+result.toString());
}
@AfterThrowing(value = "testCut()", throwing = "e")
public void afterThrow(JoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP進入方法異常攔截, 方法名為: " + method.getName() + ", 異常資訊為: " + e.getMessage());
}
}
再給出一個使用註解的方式,來標記切入點的完整案例
需要注意,這個方式需要自定義註解
1、匯入依賴
common和swagger是附加的,讀者不用必須的哈
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.gjing</groupId>
<artifactId>tools-common</artifactId>
<version>1.0.4</version>
</dependency>
<dependency>
<groupId>cn.gjing</groupId>
<artifactId>tools-starter-swagger</artifactId>
<version>1.0.9</version>
</dependency>
2、建立註解
註解用於進行AOP攔截
/**
* @author Gjing
**/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Test {
}
2、建立一個介面
/**
* @author Gjing
**/
@RestController
@RequestMapping("test1")
public class TestController {
@GetMapping("/ok")
@ApiOperation(value = "測試1", httpMethod = "GET")
@ApiImplicitParam(name = "id", value = "id值", dataType = "int", paramType = "query")
@Test
@NotNull
public String test2(Integer id) {
return "ok";
}
}
3、建立切面類
/**
* @author Gjing
**/
@Aspect
@Component
public class TestAspect {
@Pointcut("@annotation(com.gj.annotations.Test)")
public void testCut() {
}
@Before("testCut()")
public void cutProcess(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP開始攔截, 當前攔截的方法名: " + method.getName());
}
@After("testCut()")
public void after(JoinPoint joinPoint) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP執行的方法 :"+method.getName()+" 執行完了");
}
@Around("testCut()")
public Object testCutAround(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("註解方式AOP攔截開始進入環繞通知.......");
Object proceed = joinPoint.proceed();
System.out.println("準備退出環繞......");
return proceed;
}
/**
* returning屬性指定連線點方法返回的結果放置在result變數中
* @param joinPoint 連線點
* @param result 返回結果
*/
@AfterReturning(value = "testCut()",returning = "result")
public void afterReturn(JoinPoint joinPoint, Object result) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP攔截的方法執行成功, 進入返回通知攔截, 方法名為: "+method.getName()+", 返回結果為: "+result.toString());
}
@AfterThrowing(value = "testCut()", throwing = "e")
public void afterThrow(JoinPoint joinPoint, Exception e) {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
System.out.println("註解方式AOP進入方法異常攔截, 方法名為: " + method.getName() + ", 異常資訊為: " + e.getMessage());
}
}
4、啟動執行
- 正常情況:
- 異常情況: