Spring Boot之面向切面編程:Spring AOP

前言

來啦老鐵!

筆者學習Spring Boot有一段時間了,附上Spring Boot系列學習文章,歡迎取閱、賜教:

  1. 5分鐘入手Spring Boot;
  2. Spring Boot數據庫交互之Spring Data JPA;
  3. Spring Boot數據庫交互之Mybatis;
  4. Spring Boot視圖技術;
  5. Spring Boot之整合Swagger;
  6. Spring Boot之junit單元測試踩坑;
  7. 如何在Spring Boot中使用TestNG;
  8. Spring Boot之整合logback日志;
  9. Spring Boot之整合Spring Batch:批處理與任務調度;
  10. Spring Boot之整合Spring Security: 訪問認證;
  11. Spring Boot之整合Spring Security: 授權管理;
  12. Spring Boot之多數據庫源:極簡方案;
  13. Spring Boot之使用MongoDB數據庫源;
  14. Spring Boot之多線程、異步:@Async;
  15. Spring Boot之前后端分離(一):Vue前端;
  16. Spring Boot之前后端分離(二):后端、前后端集成
  17. Spring Boot之前后端分離(三):登錄、登出、頁面認證

之前在剛學習Spring Boot的時候有看到AOP,還是挺容易的,但沒有實踐一下,而近期由于某些原因,幾次被問及AOP,作為系統學習Spring Boot的咱們,當然不能落下Spring AOP!

AOP(Aspect-Oriented Programming,面向切面編程)是一種編程范式,是面向對象編程的補充,它提供了另外一種思路來實現應用系統的公共服務。AOP采用“橫切”技術,解剖已封裝的對象,將這種公共服務封裝到一個可重用的模塊中,這模塊稱之為“Aspect”,即“切面”。“切面”可降低系統代碼冗余,降低模塊間的耦合度,提升系統的可維護性。

AOP常見的使用場景:

1. 日志功能;

采用AOP之后,不需要在每一處功能中添加日志收集代碼,而是在切面中統一完成這一步驟,提升了編程速度和代碼整潔度!

2. 業務方法調用的權限管理;

采用AOP在處理權限管理,我們不用在所有業務代碼處判斷用戶是否有權限調用此方法,而是在切面中統一完成這一步驟,減少了這種非核心業務的代碼!

3. 數據庫事務的管理;

采用AOP可以統一在執行數據庫前先開啟事務,在執行完成后提交事務,若執行出錯,則回滾事務等。

4. 緩存方面;

我們可采用AOP技術,統一對數據進行緩存,在下次調用時,如果參數、條件等未變,則直接獲取數據,而不再調取應用方法。

5. 等。

AOP有點攔截的感覺!

AOP有一些術語:

  • Aspect;
  • Joint point;
  • Pointcut;
  • Advice;
  • AOP proxy;
  • Weaving;

這些術語對我們理解、實踐AOP沒有太大阻礙,請自行腦補哈,我們直接上代碼開始Demo!

項目代碼已上傳Git Hub倉庫,歡迎取閱:

整體步驟

  1. 創建AOP演示項目;
  2. 引入AOP依賴;
  3. 創建演示用API;
  4. 編寫AOP切面類;
  5. 驗證AOP代碼織入效果;

1. 創建AOP演示項目;

Spring Boot項目創建可參考文章:5分鐘入手Spring Boot,此處不再介紹。

2. 引入AOP依賴;

在項目pom.xml中添加spring-boot-starter-aop依賴:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
記得安裝一下依賴:
mvn install -Dmaven.test.skip=true -Dmaven.wagon.http.ssl.insecure=true -Dmaven.wagon.http.ssl.allowall=true

3. 創建演示用API;

項目內創建controller包,包內創建一個controller,如HelloWorldController.java,HelloWorldController內創建一個用于演示用的API:

package com.github.dylanz666.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author : dylanz
 * @since : 10/22/2020
 */
@RestController
public class HelloWorldController {
    @GetMapping("/api/hello")
    public String sayHello(@RequestParam String user) {
        return "Hello " + user;
    }
}

此處特地寫了一個需要參數的API,我將把API處理過程進行橫切,在API請求前后做一些系統級別的操作,但不影響業務過程。

4. 編寫AOP切面類;

在項目內創建config包,在包內創建一個config類,如AOPConfig.java,在AOPConfig編寫如下代碼:

package com.github.dylanz666.config;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Arrays;

/**
 * @author : dylanz
 * @since : 10/22/2020
 */
@Configuration
@Aspect
public class AOPConfig {
    @Around("@within(org.springframework.web.bind.annotation.RestController)")
    public Object simpleAop(final ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        assert attributes != null;
        HttpServletRequest request = attributes.getRequest();
        System.out.println("client ip:" + request.getRemoteAddr());

        Object[] args = proceedingJoinPoint.getArgs();
        System.out.println("args:" + Arrays.asList(args));

        Object object = proceedingJoinPoint.proceed();
        System.out.println("return: " + object);

        return object;
    }
}

稍微解讀一下:

1). @Aspect,聲明了這個類是個切面類;
2). @Around,聲明了一個表達式,描述了要織入的目標特性;

比如本例@within表示目標類型帶有注解,且其注解類型為 org.springframework.web.bind.annotation.RestController(如果API的注解用的是@Conrtoller,則此處為 org.springframework.stereotype.Controller),這樣系統內所有RestController方法(Rest API,也即帶有@RestController注解的controller類中的方法)被調用的時候,都會執行@Around注解的方法,也就是本例的simpleAop方法;
除了@Around(方法執行前后織入代碼),還有@Before、@After、@AfterReturning、@AfterThrowing,他們均分別表示該織入代碼用于執行方法前、執行方法后、方法返回后、方法拋出異常后,如:

@Before("@within(org.springframework.web.bind.annotation.RestController)")
public void before(JoinPoint joinPoint) throws Throwable {
    Object[] args = joinPoint.getArgs();
    System.out.println("args:" + Arrays.asList(args));

    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    assert attributes != null;
    HttpServletRequest request = attributes.getRequest();
    System.out.println("client ip:" + request.getRemoteAddr());
}

我們在執行方法前打印請求參數和客戶端ip;

3). 除了Around注解的方法可以傳ProceedingJionPoint類型的參數外,其余的幾個都不能傳ProceedingJionPoint類型的參數;
4). simpleAop(名字任意)是用來織入的代碼,我們可以利用參數ProceedingJoinPoint提供的方法,來對請求前后進行系統級別操作。

例如本例在接收到API請求還未執行業務代碼時將客戶端ip、請求參數打印出來,然后在業務代碼執行完成后未返回給客戶端前,將返回結果先打印出來;

5). 通常當切面代碼執行完后,我們需要繼續執行應用代碼,并將返回對象正常返回,Object object = proceedingJoinPoint.proceed();就是為了完成這一過程;
6). 除了@within這種切面目標匹配表達式外,Spring AOP還提供了多種可選的表達式及表達式組合:
(1). within();
(2). @within;
(3). execution(),如:
  • execution(public * *(...));
  • execution(* set*(...));
  • execution(public set*(...));
  • execution(public com.xyz.service..set(...));
(4). target();
(5). @target;
(6). args();
(7). @args();
(8). @annotation();
(9). this();
(10). @Transactional;

等,讀者可自行展開學習!

5. 驗證AOP代碼織入效果;

1). 項目整體結構:

項目整體結構

2). 啟動項目:

啟動項目

3). 訪問API:

(手機在局域網內訪問我們的應用路徑:http://192.168.0.101:8080/api/hello?user=dylanz)

訪問API

4). 后端執行切面代碼:

后端執行切面代碼

我們可以看到,在API執行前,打印了手機的ip地址:192.168.0.100,同時打印了請求參數值:dylanz,在API對應的方法執行后,打印方法返回的Hello dylanz字符串給客戶端,然后將該字符串傳給客戶端,之后我們便能在手機客戶端看到Hello dylanz字符串!

不難看出,我們可以將這些打印換成日志打印,就能全局收集詳細的信息!
或者也可在切面中做一些緩存操作、數據庫事務方面的行為等。

至此,我們完成了一個簡單的Spring AOP案例,整個過程簡單而不失靈活,靈活而不失優雅,有沒有?

如果本文對您有幫助,麻煩點贊+關注!

謝謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。