前言
來啦老鐵!
筆者學習Spring Boot有一段時間了,附上Spring Boot系列學習文章,歡迎取閱、賜教:
- 5分鐘入手Spring Boot;
- Spring Boot數據庫交互之Spring Data JPA;
- Spring Boot數據庫交互之Mybatis;
- Spring Boot視圖技術;
- Spring Boot之整合Swagger;
- Spring Boot之junit單元測試踩坑;
- 如何在Spring Boot中使用TestNG;
- Spring Boot之整合logback日志;
- Spring Boot之整合Spring Batch:批處理與任務調度;
- Spring Boot之整合Spring Security: 訪問認證;
- Spring Boot之整合Spring Security: 授權管理;
- Spring Boot之多數據庫源:極簡方案;
- Spring Boot之使用MongoDB數據庫源;
- Spring Boot之多線程、異步:@Async;
- Spring Boot之前后端分離(一):Vue前端;
- Spring Boot之前后端分離(二):后端、前后端集成
- 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倉庫,歡迎取閱:
整體步驟
- 創建AOP演示項目;
- 引入AOP依賴;
- 創建演示用API;
- 編寫AOP切面類;
- 驗證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)
4). 后端執行切面代碼:
我們可以看到,在API執行前,打印了手機的ip地址:192.168.0.100,同時打印了請求參數值:dylanz,在API對應的方法執行后,打印方法返回的Hello dylanz字符串給客戶端,然后將該字符串傳給客戶端,之后我們便能在手機客戶端看到Hello dylanz字符串!
不難看出,我們可以將這些打印換成日志打印,就能全局收集詳細的信息!
或者也可在切面中做一些緩存操作、數據庫事務方面的行為等。
至此,我們完成了一個簡單的Spring AOP案例,整個過程簡單而不失靈活,靈活而不失優雅,有沒有?
如果本文對您有幫助,麻煩點贊+關注!
謝謝!