Spring Boot AOP 應(yīng)用場(chǎng)景
1. 前言
Spring 最重要的兩個(gè)功能,就是依賴注入(DI)和面向切面編程 (AOP)。
AOP 為我們提供了處理問(wèn)題的全局化視角,使用得當(dāng)可以極大提高編程效率。
Spring Boot 中使用 AOP 與 Spring 中使用 AOP 幾乎沒(méi)有什么區(qū)別,只是建議盡量使用 Java 配置代替 XML 配置。
本節(jié)就來(lái)演示下 Spring Boot 中使用 AOP 的常見應(yīng)用場(chǎng)景。
2. 構(gòu)建項(xiàng)目
首先我們需要構(gòu)建一個(gè) Spring Boot 項(xiàng)目并引入 AOP 依賴,后續(xù)場(chǎng)景演示均是在這個(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ù)訪問(wèn)層
為了便于后續(xù)的演示,我們依次新建控制類、服務(wù)類、數(shù)據(jù)訪問(wèn)類,并將其放入對(duì)應(yīng)的包中,項(xiàng)目結(jié)構(gòu)如下:
各個(gè)類代碼如下,注意此處僅僅是為了演示 AOP 的使用,并未真實(shí)訪問(wèn)數(shù)據(jù)庫(kù),而是直接返回了測(cè)試數(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ù)庫(kù)訪問(wèn)類
*/
@Repository // 標(biāo)注數(shù)據(jù)訪問(wèn)類
public class GoodsDao {
/**
* 查詢商品列表
*/
public List getList() {
return new ArrayList();
}
}
3. 使用 AOP 記錄日志
如果要記錄對(duì)控制器接口的訪問(wèn)日志,可以定義一個(gè)切面,切入點(diǎn)即為控制器中的接口方法,然后通過(guò)前置通知來(lái)打印日志。
實(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("訪問(wèn)時(shí)間:{}--訪問(wèn)接口:{}", new Date(), joinPoint.getSignature());
}
}
啟動(dòng)項(xiàng)目后,訪問(wèn)控制器中的方法之前會(huì)先執(zhí)行 doBefore 方法??刂婆_(tái)打印如下:
2020-05-25 22:14:12.317 INFO 9992 --- [nio-8080-exec-2] com.imooc.springbootaop.LogAspect :
訪問(wèn)時(shí)間:Mon May 25 22:14:12 CST 2020--訪問(wèn)接口:List com.imooc.springbootaop.controller.GoodsController.getList()
4. 使用 AOP 監(jiān)控性能
在研發(fā)項(xiàng)目的性能測(cè)試階段,或者項(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:正常情況下,用戶查看頁(yè)面或進(jìn)行更新操作時(shí),耗時(shí)超過(guò) 1.5 秒,就會(huì)感覺到明顯的遲滯感。由于前后端交互也需要耗時(shí),按正態(tài)分布的話,大部分交互耗時(shí)在 0.4秒 左右。所以在我參與的項(xiàng)目中,會(huì)對(duì)耗時(shí)超過(guò) 1.1 秒的服務(wù)層方法進(jìn)行跟蹤分析,通過(guò)優(yōu)化 SQL 語(yǔ)句、優(yōu)化算法、添加緩存等方式縮短方法執(zhí)行時(shí)間。上面的數(shù)值均為我個(gè)人的經(jīng)驗(yàn)參考值,還要視乎具體的服務(wù)器、網(wǎng)絡(luò)、應(yīng)用場(chǎng)景來(lái)確定合理的監(jiān)控臨界值。
5. 使用 AOP 統(tǒng)一后端返回值格式
前后端分離的項(xiàng)目結(jié)構(gòu)中,前端通過(guò) Ajax 請(qǐng)求后端接口,此時(shí)最好使用統(tǒng)一的返回值格式供前端處理。此處就可以借助 AOP 來(lái)實(shí)現(xiàn)正常情況、異常情況返回值的格式統(tǒng)一。
5.1 定義返回值類
首先定義返回值類,它屬于業(yè)務(wù)邏輯對(duì)象 (Bussiness Object),所以此處命名為 ResultBo ,代碼如下:
實(shí)例:
public class ResultBo<T> {
/**
* 錯(cuò)誤碼 0表示沒(méi)有錯(cuò)誤(異常) 其他數(shù)字代表具體錯(cuò)誤碼
*/
private int code;
/**
* 后端返回消息
*/
private String msg;
/**
* 后端返回的數(shù)據(jù)
*/
private T data;
/**
* 無(wú)參數(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)行修改,保證返回值均通過(guò) 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 測(cè)試
啟動(dòng)項(xiàng)目,訪問(wèn) http://127.0.0.1:8080/goods
返回?cái)?shù)據(jù)如下:
實(shí)例:
{"code":0,"msg":"操作成功","data":[]}
然后訪問(wèn) http://127.0.0.1:8080/test
,返回?cái)?shù)據(jù)如下:
實(shí)例:
{"code":99999,"msg":"/ by zero","data":null}
這樣,前端可以根據(jù)返回值的 code, 來(lái)判斷后端是否正常響應(yīng)。如果 code 為 0 ,則進(jìn)行正常業(yè)務(wù)邏輯操作;如果 code 非 0 ,則可以彈窗顯示 msg 提示信息。
6. 小結(jié)
AOP 之所以如此重要,在于它提供了解決問(wèn)題的新視角。通過(guò)將業(yè)務(wù)邏輯抽象出切面,功能代碼可以切入指定位置,從而消除重復(fù)的模板代碼。
使用 AOP 有一種掌握全局的快感,發(fā)現(xiàn)業(yè)務(wù)邏輯中的切面頗有一番趣味,希望大家都能多多體會(huì),編程且快樂(lè)著應(yīng)該是我輩的追求。