1. 程式人生 > 其它 >再來一片文章實戰 AOP

再來一片文章實戰 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)
, 任意數量的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、啟動執行

  • 正常情況
    y
  • 異常情況
    error