1. 前言
Spring 最重要的兩個功能倘潜,就是依賴注入(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)建一個 Spring Boot 項(xiàng)目并引入 AOP 依賴既忆,后續(xù)場景演示均是在這個項(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ù)訪問類援所,并將其放入對應(yīng)的包中,項(xiàng)目結(jié)構(gòu)如下:
項(xiàng)目結(jié)構(gòu)
各個類代碼如下欣除,注意此處僅僅是為了演示 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 記錄日志
如果要記錄對控制器接口的訪問日志废酷,可以定義一個切面瘟檩,切入點(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("訪問時間:{}--訪問接口:{}", new Date(), joinPoint.getSignature());
}
}
啟動項(xiàng)目后,訪問控制器中的方法之前會先執(zhí)行 doBefore 方法趴俘《么兀控制臺打印如下:
2020-05-25 22:14:12.317 INFO 9992 --- [nio-8080-exec-2] com.imooc.springbootaop.LogAspect :
訪問時間:Mon May 25 22:14:12 CST 2020--訪問接口:List com.imooc.springbootaop.controller.GoodsController.getList()
4. 使用 AOP 監(jiān)控性能
在研發(fā)項(xiàng)目的性能測試階段,或者項(xiàng)目部署后寥闪,我們會希望查看服務(wù)層方法執(zhí)行的時間太惠。以便精準(zhǔn)的了解項(xiàng)目中哪些服務(wù)方法執(zhí)行速度慢,后續(xù)可以針對性的進(jìn)行性能優(yōu)化疲憋。
此時我們就可以使用 AOP 的環(huán)繞通知凿渊,監(jiān)控服務(wù)方法的執(zhí)行時間。
實(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();// 記錄開始時間
Object result = joinPoint.proceed();
logger.info("服務(wù)層方法:{}--執(zhí)行時間:{}毫秒", joinPoint.getSignature(), System.currentTimeMillis() - startTime);
return result;
}
}
當(dāng)服務(wù)層方法被調(diào)用時埃脏,控制臺輸入日志如下:
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í)行時間:3毫秒
Tips:正常情況下,用戶查看頁面或進(jìn)行更新操作時秋忙,耗時超過 1.5 秒彩掐,就會感覺到明顯的遲滯感。由于前后端交互也需要耗時灰追,按正態(tài)分布的話堵幽,大部分交互耗時在 0.4秒 左右。所以在我參與的項(xiàng)目中弹澎,會對耗時超過 1.1 秒的服務(wù)層方法進(jìn)行跟蹤分析谐檀,通過優(yōu)化 SQL 語句、優(yōu)化算法裁奇、添加緩存等方式縮短方法執(zhí)行時間桐猬。上面的數(shù)值均為我個人的經(jīng)驗(yàn)參考值,還要視乎具體的服務(wù)器刽肠、網(wǎng)絡(luò)溃肪、應(yīng)用場景來確定合理的監(jiān)控臨界值。
5. 使用 AOP 統(tǒng)一后端返回值格式
前后端分離的項(xiàng)目結(jié)構(gòu)中音五,前端通過 Ajax 請求后端接口惫撰,此時最好使用統(tǒng)一的返回值格式供前端處理。此處就可以借助 AOP 來實(shí)現(xiàn)正常情況躺涝、異常情況返回值的格式統(tǒng)一厨钻。
5.1 定義返回值類
首先定義返回值類,它屬于業(yè)務(wù)邏輯對象 (Bussiness Object),所以此處命名為 ResultBo 夯膀,代碼如下:
實(shí)例:
public class ResultBo<T> {
/**
* 錯誤碼 0表示沒有錯誤(異常) 其他數(shù)字代表具體錯誤碼
*/
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 修改控制層返回值類型
對所有的控制層方法進(jìn)行修改诗充,保證返回值均通過 ResultBo 包裝,另外我們再定義一個方法诱建,模擬拋出異常的控制層方法蝴蜓。
實(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 類型對象俺猿,然后我們需要定義切面茎匠,處理控制層拋出的異常。當(dāng)發(fā)生異常時押袍,同樣返回 ResultBo 類型的對象诵冒,并且對象中包含異常信息。
實(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í)行異常時汽馋,返回ResultBo
}
}
}
5.4 測試
啟動項(xiàng)目,訪問 http://127.0.0.1:8080/goods
返回數(shù)據(jù)如下:
實(shí)例:
{"code":0,"msg":"操作成功","data":[]}
然后訪問 http://127.0.0.1:8080/test
午笛,返回數(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ù)邏輯中的切面頗有一番趣味吱殉,希望大家都能多多體會掸冤,編程且快樂著應(yīng)該是我輩的追求。