Spring Boot AOP 應(yīng)用場景
1. 前言
Spring 最重要的兩個(gè)功能,就是依賴注入(DI)和面向切面編程 (AOP)。
AOP 為我們提供了處理問題的全局化視角,使用得當(dāng)可以極大提高編程效率。
Spring Boot 中使用 AOP 與 Spring 中使用 AOP 幾乎沒有什么區(qū)別,只是建議盡量使用 Java 配置代替 XML 配置。
本節(jié)就來演示下 Spring Boot 中使用 AOP 的常見應(yīng)用場景。
2. 構(gòu)建項(xiàng)目
首先我們需要構(gòu)建一個(gè) Spring Boot 項(xiàng)目并引入 AOP 依賴,后續(xù)場景演示均是在這個(gè)項(xiàng)目上實(shí)現(xiàn)的。
2.1 使用 Spring Initializr 創(chuàng)建項(xiàng)目
Spring Boot 版本選擇 2.2.5 ,Group 為 com.imooc , Artifact 為 spring-boot-aop,生成項(xiàng)目后導(dǎo)入 Eclipse 開發(fā)環(huán)境。
2.2 引入項(xiàng)目依賴
我們引入 Web 項(xiàng)目依賴與 AOP 依賴。
實(shí)例:
<!-- Web項(xiàng)目依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
2.3 新建控制層、服務(wù)層、數(shù)據(jù)訪問層
為了便于后續(xù)的演示,我們依次新建控制類、服務(wù)類、數(shù)據(jù)訪問類,并將其放入對(duì)應(yīng)的包中,項(xiàng)目結(jié)構(gòu)如下:

各個(gè)類代碼如下,注意此處僅僅是為了演示 AOP 的使用,并未真實(shí)訪問數(shù)據(jù)庫,而是直接返回了測試數(shù)據(jù)。
實(shí)例:
/**
* 商品控制器類
*/
@RestController
public class GoodsController {
@Autowired
private GoodsService goodsService;
/**
* 獲取商品列表
*/
@GetMapping("/goods")
public List getList() {
return goodsService.getList();
}
}
實(shí)例:
/**
* 商品服務(wù)類
*/
@Service
public class GoodsService {
@Autowired
private GoodsDao goodsDao;
/**
* 獲取商品信息列表
*/
public List getList() {
return goodsDao.getList();
}
}
實(shí)例:
/**
* 商品數(shù)據(jù)庫訪問類
*/
@Repository // 標(biāo)注數(shù)據(jù)訪問類
public class GoodsDao {
/**
* 查詢商品列表
*/
public List getList() {
return new ArrayList();
}
}
3. 使用 AOP 記錄日志
如果要記錄對(duì)控制器接口的訪問日志,可以定義一個(gè)切面,切入點(diǎn)即為控制器中的接口方法,然后通過前置通知來打印日志。
實(shí)例:
/**
* 日志切面
*/
@Component
@Aspect // 標(biāo)注為切面
public class LogAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 切入點(diǎn)表達(dá)式,表示切入點(diǎn)為控制器包中的所有方法
@Pointcut("within(com.imooc.springbootaop.controller..*)")
public void LogAspect() {
}
// 切入點(diǎn)之前執(zhí)行
@Before("LogAspect()")
public void doBefore(JoinPoint joinPoint) {
logger.info("訪問時(shí)間:{}--訪問接口:{}", new Date(), joinPoint.getSignature());
}
}
啟動(dòng)項(xiàng)目后,訪問控制器中的方法之前會(huì)先執(zhí)行 doBefore 方法。控制臺(tái)打印如下:
2020-05-25 22:14:12.317 INFO 9992 --- [nio-8080-exec-2] com.imooc.springbootaop.LogAspect :
訪問時(shí)間:Mon May 25 22:14:12 CST 2020--訪問接口:List com.imooc.springbootaop.controller.GoodsController.getList()
4. 使用 AOP 監(jiān)控性能
在研發(fā)項(xiàng)目的性能測試階段,或者項(xiàng)目部署后,我們會(huì)希望查看服務(wù)層方法執(zhí)行的時(shí)間。以便精準(zhǔn)的了解項(xiàng)目中哪些服務(wù)方法執(zhí)行速度慢,后續(xù)可以針對(duì)性的進(jìn)行性能優(yōu)化。
此時(shí)我們就可以使用 AOP 的環(huán)繞通知,監(jiān)控服務(wù)方法的執(zhí)行時(shí)間。
實(shí)例:
/**
* 服務(wù)層方法切面
*/
@Component
@Aspect // 標(biāo)注為切面
public class ServiceAspect {
private Logger logger = LoggerFactory.getLogger(this.getClass());
// 切入點(diǎn)表達(dá)式,表示切入點(diǎn)為服務(wù)層包中的所有方法
@Pointcut("within(com.imooc.springbootaop.service..*)")
public void ServiceAspect() {
}
@Around("ServiceAspect()") // 環(huán)繞通知
public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();// 記錄開始時(shí)間
Object result = joinPoint.proceed();
logger.info("服務(wù)層方法:{}--執(zhí)行時(shí)間:{}毫秒", joinPoint.getSignature(), System.currentTimeMillis() - startTime);
return result;
}
}
當(dāng)服務(wù)層方法被調(diào)用時(shí),控制臺(tái)輸入日志如下:
2020-05-25 22:25:56.830 INFO 4800 --- [nio-8080-exec-1] com.imooc.springbootaop.ServiceAspect :
服務(wù)層方法:List com.imooc.springbootaop.service.GoodsService.getList()--執(zhí)行時(shí)間:3毫秒
Tips:正常情況下,用戶查看頁面或進(jìn)行更新操作時(shí),耗時(shí)超過 1.5 秒,就會(huì)感覺到明顯的遲滯感。由于前后端交互也需要耗時(shí),按正態(tài)分布的話,大部分交互耗時(shí)在 0.4秒 左右。所以在我參與的項(xiàng)目中,會(huì)對(duì)耗時(shí)超過 1.1 秒的服務(wù)層方法進(jìn)行跟蹤分析,通過優(yōu)化 SQL 語句、優(yōu)化算法、添加緩存等方式縮短方法執(zhí)行時(shí)間。上面的數(shù)值均為我個(gè)人的經(jīng)驗(yàn)參考值,還要視乎具體的服務(wù)器、網(wǎng)絡(luò)、應(yīng)用場景來確定合理的監(jiān)控臨界值。
5. 使用 AOP 統(tǒng)一后端返回值格式
前后端分離的項(xiàng)目結(jié)構(gòu)中,前端通過 Ajax 請(qǐng)求后端接口,此時(shí)最好使用統(tǒng)一的返回值格式供前端處理。此處就可以借助 AOP 來實(shí)現(xiàn)正常情況、異常情況返回值的格式統(tǒng)一。
5.1 定義返回值類
首先定義返回值類,它屬于業(yè)務(wù)邏輯對(duì)象 (Bussiness Object),所以此處命名為 ResultBo ,代碼如下:
實(shí)例:
public class ResultBo<T> {
/**
* 錯(cuò)誤碼 0表示沒有錯(cuò)誤(異常) 其他數(shù)字代表具體錯(cuò)誤碼
*/
private int code;
/**
* 后端返回消息
*/
private String msg;
/**
* 后端返回的數(shù)據(jù)
*/
private T data;
/**
* 無參數(shù)構(gòu)造函數(shù)
*/
public ResultBo() {
this.code = 0;
this.msg = "操作成功";
}
/**
* 帶數(shù)據(jù)data構(gòu)造函數(shù)
*/
public ResultBo(T data) {
this();
this.data = data;
}
/**
* 存在異常的構(gòu)造函數(shù)
*/
public ResultBo(Exception ex) {
this.code = 99999;// 其他未定義異常
this.msg = ex.getMessage();
}
// 省略 get set
}
5.2 修改控制層返回值類型
對(duì)所有的控制層方法進(jìn)行修改,保證返回值均通過 ResultBo 包裝,另外我們?cè)俣x一個(gè)方法,模擬拋出異常的控制層方法。
實(shí)例:
/**
* 獲取商品列表
*/
@GetMapping("/goods")
public ResultBo getList() {
return new ResultBo(goodsService.getList());
}
/**
* 模擬拋出異常的方法
*/
@GetMapping("/test")
public ResultBo test() {
int a = 1 / 0;
return new ResultBo(goodsService.getList());
}
5.3 定義切面處理異常返回值
正??刂茖臃椒ǘ挤祷?ResultBo 類型對(duì)象,然后我們需要定義切面,處理控制層拋出的異常。當(dāng)發(fā)生異常時(shí),同樣返回 ResultBo 類型的對(duì)象,并且對(duì)象中包含異常信息。
實(shí)例:
/**
* 返回值切面
*/
@Component
@Aspect
public class ResultAspect {
// 切入點(diǎn)表達(dá)式,表示切入點(diǎn)為返回類型ResultBo的所有方法
@Pointcut("execution(public com.imooc.springbootaop.ResultBo *(..))")
public void ResultAspect() {
}
// 環(huán)繞通知
@Around("ResultAspect()")
public Object deAround(ProceedingJoinPoint joinPoint) throws Throwable {
try {
return joinPoint.proceed();// 返回正常結(jié)果
} catch (Exception ex) {
return new ResultBo<>(ex);// 被切入的方法執(zhí)行異常時(shí),返回ResultBo
}
}
}
5.4 測試
啟動(dòng)項(xiàng)目,訪問 http://127.0.0.1:8080/goods 返回?cái)?shù)據(jù)如下:
實(shí)例:
{"code":0,"msg":"操作成功","data":[]}
然后訪問 http://127.0.0.1:8080/test ,返回?cái)?shù)據(jù)如下:
實(shí)例:
{"code":99999,"msg":"/ by zero","data":null}
這樣,前端可以根據(jù)返回值的 code, 來判斷后端是否正常響應(yīng)。如果 code 為 0 ,則進(jìn)行正常業(yè)務(wù)邏輯操作;如果 code 非 0 ,則可以彈窗顯示 msg 提示信息。
6. 小結(jié)
AOP 之所以如此重要,在于它提供了解決問題的新視角。通過將業(yè)務(wù)邏輯抽象出切面,功能代碼可以切入指定位置,從而消除重復(fù)的模板代碼。
使用 AOP 有一種掌握全局的快感,發(fā)現(xiàn)業(yè)務(wù)邏輯中的切面頗有一番趣味,希望大家都能多多體會(huì),編程且快樂著應(yīng)該是我輩的追求。
程序員大陽 ·
2025 imooc.com All Rights Reserved |