Spring AOP 實戰運用

Spring AOP 實戰

看了上面這么多的理論知識, 不知道大家有沒有覺得枯燥哈. 不過不要急, 俗話說理論是實踐的基礎, 對 Spring AOP 有了基本的理論認識后, 我們來看一下下面幾個具體的例子吧.下面的幾個例子是我在工作中所遇見的比較常用的 Spring AOP 的使用場景, 我精簡了很多有干擾我們學習的注意力的細枝末節, 以力求整個例子的簡潔性.

下面幾個 Demo 的源碼都可以在我的 Github 上下載到.

HTTP 接口鑒權

首先讓我們來想象一下如下場景: 我們需要提供的 HTTP RESTful 服務, 這個服務會提供一些比較敏感的信息, 因此對于某些接口的調用會進行調用方權限的校驗, 而某些不太敏感的接口則不設置權限, 或所需要的權限比較低(例如某些監控接口, 服務狀態接口等).
實現這樣的需求的方法有很多, 例如我們可以在每個 HTTP 接口方法中對服務請求的調用方進行權限的檢查, 當調用方權限不符時, 方法返回錯誤. 當然這樣做并無不可, 不過如果我們的 api 接口很多, 每個接口都進行這樣的判斷, 無疑有很多冗余的代碼, 并且很有可能有某個粗心的家伙忘記了對調用者的權限進行驗證, 這樣就會造成潛在的 bug.
那么除了上面的所說的方法外, 還有沒有別的比較優雅的方式來實現呢? 當然有啦, 不然我在這啰嗦半天干嘛呢, 它就是我們今天的主角: AOP.

讓我們來提煉一下我們的需求:

  1. 可以定制地為某些指定的 HTTP RESTful api 提供權限驗證功能.

  2. 當調用方的權限不符時, 返回錯誤.

根據上面所提出的需求, 我們可以進行如下設計:

  1. 提供一個特殊的注解 AuthChecker, 這個是一個方法注解, 有此注解所標注的 Controller 需要進行調用方權限的認證.

  2. 利用 Spring AOP, 以 @annotation 切點標志符來匹配有注解 AuthChecker 所標注的 joinpoint.

  3. 在 advice 中, 簡單地檢查調用者請求中的 Cookie 中是否有我們指定的 token, 如果有, 則認為此調用者權限合法, 允許調用, 反之權限不合法, 范圍錯誤.

根據上面的設計, 我們來看一下具體的源碼吧.
首先是 AuthChecker 注解的定義:
AuthChecker.java:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthChecker {
}

AuthChecker 注解是一個方法注解, 它用于注解 RequestMapping 方法.

有了注解的定義, 那我們再來看一下 aspect 的實現吧:
HttpAopAdviseDefine.java:

@Component
@Aspect
public class HttpAopAdviseDefine {// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.@Pointcut("@annotation(com.xys.demo1.AuthChecker)")public void pointcut() {}// 定義 advise@Around("pointcut()")public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 檢查用戶所傳遞的 token 是否合法String token = getUserToken(request);if (!token.equalsIgnoreCase("123456")) {return "錯誤, 權限不合法!";}return joinPoint.proceed();}private String getUserToken(HttpServletRequest request) {Cookie[] cookies = request.getCookies();if (cookies == null) {return "";}for (Cookie cookie : cookies) {if (cookie.getName().equalsIgnoreCase("user_token")) {return cookie.getValue();}}return "";}
}

在這個 aspect 中, 我們首先定義了一個 pointcut, 以 @annotation 切點標志符來匹配有注解 AuthChecker 所標注的 joinpoint, 即:

// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.
@Pointcut("@annotation(com.xys.demo1.AuthChecker)")
public void pointcut() {
}

然后再定義一個 advice:

// 定義 advise
@Around("pointcut()")
public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 檢查用戶所傳遞的 token 是否合法String token = getUserToken(request);if (!token.equalsIgnoreCase("123456")) {return "錯誤, 權限不合法!";}return joinPoint.proceed();
}

當被 AuthChecker 注解所標注的方法調用前, 會執行我們的這個 advice, 而這個 advice 的處理邏輯很簡單, 即從 HTTP 請求中獲取名為 user_token 的 cookie 的值, 如果它的值是 123456, 則我們認為此 HTTP 請求合法, 進而調用 joinPoint.proceed() 將 HTTP 請求轉交給相應的控制器處理; 而如果user_token cookie 的值不是 123456, 或為空, 則認為此 HTTP 請求非法, 返回錯誤.

接下來我們來寫一個模擬的 HTTP 接口:
DemoController.java:

@RestController
public class DemoController {@RequestMapping("/aop/http/alive")public String alive() {return "服務一切正常";}@AuthChecker@RequestMapping("/aop/http/user_info")public String callSomeInterface() {return "調用了 user_info 接口.";}
}

注意到上面我們提供了兩個 HTTP 接口, 其中 接口 /aop/http/alive 是沒有 AuthChecker 標注的, 而 /aop/http/user_info 接口則用到了 @AuthChecker 標注. 那么自然地, 當請求了 /aop/http/user_info 接口時, 就會觸發我們所設置的權限校驗邏輯.

接下來我們來驗證一下, 我們所實現的功能是否有效吧.
首先在 Postman 中, 調用 /aop/http/alive 接口, 請求頭中不加任何參數:

可以看到, 我們的 HTTP 請求完全沒問題.

那么再來看一下請求 /aop/http/user_info 接口會怎樣呢:

當我們請求 /aop/http/user_info 接口時, 服務返回一個權限異常的錯誤, 為什么會這樣呢? 自然就是我們的權限認證系統起了作為: 當一個方法被調用并且這個方法有 AuthChecker 標注時, 那么首先會執行到我們的 around advice, 在這個 advice 中, 我們會校驗 HTTP 請求的 cookie 字段中是否有攜帶 user_token 字段時, 如果沒有, 則返回權限錯誤.
那么為了能夠正常地調用 /aop/http/user_info 接口, 我們可以在 Cookie 中添加 user_token=123456, 這樣我們可以愉快的玩耍了:

注意, Postman 默認是不支持 Cookie 的, 所以為了實現添加 Cookie 的功能, 我們需要安裝 Postman 的 interceptor 插件. 安裝方法可以看官網的文章

完整源碼

HTTP 接口鑒權

方法調用日志

第二個 AOP 實例是記錄一個方法調用的log. 這應該是一個很常見的功能了.首先假設我們有如下需求:

  1. 某個服務下的方法的調用需要有 log: 記錄調用的參數以及返回結果.

  2. 當方法調用出異常時, 有特殊處理, 例如打印異常 log, 報警等.

根據上面的需求, 我們可以使用 before advice 來在調用方法前打印調用的參數, 使用 after returning advice 在方法返回打印返回的結果. 而當方法調用失敗后, 可以使用 after throwing advice 來做相應的處理.那么我們來看一下 aspect 的實現:

@Component
@Aspect
public class LogAopAdviseDefine {private Logger logger = LoggerFactory.getLogger(getClass());// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.@Pointcut("within(NeedLogService)")public void pointcut() {}// 定義 advise@Before("pointcut()")public void logMethodInvokeParam(JoinPoint joinPoint) {logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());}@AfterReturning(pointcut = "pointcut()", returning = "retVal")public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());}@AfterThrowing(pointcut = "pointcut()", throwing = "exception")public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());}
}

第一步, 自然是定義一個 pointcut, 以 within 切點標志符來匹配類 NeedLogService 下的所有 joinpoint, 即:

@Pointcut("within(NeedLogService)")
public void pointcut() {
}

接下來根據我們前面的設計, 我們分別定義了三個 advice, 第一個是一個 before advice:

@Before("pointcut()")
public void logMethodInvokeParam(JoinPoint joinPoint) {logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}

它在一個符合要求的 joinpoint 方法調用前執行, 打印調用的方法名和調用的參數.

第二個是 after return advice:

@AfterReturning(pointcut = "pointcut()", returning = "retVal")
public void logMethodInvokeResult(JoinPoint joinPoint, Object retVal) {logger.info("---After method {} invoke, result: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs());
}

這個 advice 會在方法調用成功后打印出方法名還反的參數.

最后一個是 after throw advice:

@AfterThrowing(pointcut = "pointcut()", throwing = "exception")
public void logMethodInvokeException(JoinPoint joinPoint, Exception exception) {logger.info("---method {} invoke exception: {}---", joinPoint.getSignature().toShortString(), exception.getMessage());
}

這個 advice 會在指定的 joinpoint 拋出異常時執行, 打印異常的信息.

接下來我們再寫兩個 Service 類:
NeedLogService.java:

@Service
public class NeedLogService {private Logger logger = LoggerFactory.getLogger(getClass());private Random random = new Random(System.currentTimeMillis());public int logMethod(String someParam) {logger.info("---NeedLogService: logMethod invoked, param: {}---", someParam);return random.nextInt();}public void exceptionMethod() throws Exception {logger.info("---NeedLogService: exceptionMethod invoked---");throw new Exception("Something bad happened!");}
}

NormalService.java:

@Service
public class NormalService {private Logger logger = LoggerFactory.getLogger(getClass());public void someMethod() {logger.info("---NormalService: someMethod invoked---");}
}

根據我們 pointcut 的規則, 類 NeedLogService 下的所有方法都會被織入 advice, 而類 NormalService 則不會.

最后我們分別調用這幾個方法:

@PostConstruct
public void test() {needLogService.logMethod("xys");try {needLogService.exceptionMethod();} catch (Exception e) {// Ignore}normalService.someMethod();
}

我們可以看到有如下輸出:

---Before method NeedLogService.logMethod(..) invoke, param: [xys]---
---NeedLogService: logMethod invoked, param: xys---
---After method NeedLogService.logMethod(..) invoke, result: [xys]------Before method NeedLogService.exceptionMethod() invoke, param: []---
---NeedLogService: exceptionMethod invoked---
---method NeedLogService.exceptionMethod() invoke exception: Something bad happened!------NormalService: someMethod invoked---

根據 log, 我們知道, NeedLogService.logMethod 執行的前后確實有 advice 執行了, 并且在 NeedLogService.exceptionMethod 拋出異常后, logMethodInvokeException 這個 advice 也被執行了. 而由于 pointcut 的匹配規則, 在 NormalService 類中的方法則不會織入 advice.

完整源碼

方法調用日志

方法耗時統計

作為程序員, 我們都知道服務監控對于一個服務能夠長期穩定運行的重要性, 因此很多公司都有自己內部的監控報警系統, 或者是使用一些開源的系統, 例如小米的 Falcon 監控系統.

那么在程序監控中, AOP 有哪些用武之地呢? 我們來假想一下如下場景:

有一天, leader 對小王說, "小王啊, 你負責的那個服務不太穩定啊, 經常有超時發生! 你有對這些服務接口進行過耗時統計嗎?"
耗時統計? 小王嘀咕了, 小聲的回答到: "還沒有加呢."
leader: "你看著辦吧, 我明天要看到各個時段的服務接口調用的耗時分布!"
小王這就犯難了, 雖然說計算一個方法的調用耗時并不是一個很難的事情, 但是整個服務有二十來個接口呢, 一個一個地添加統計代碼, 那還不是要累死人了.
看著同事一個一個都下班回家了, 小王眉頭更加緊了. 不過此時小王靈機一動: "噫, 有了!".
小王想到了一個好方法, 立即動手, 吭哧吭哧地幾分鐘就搞定了.

那么小王的解決方法是什么呢? 自然是我們的主角 AOP 啦.

首先讓我們來提煉一下需求:

  1. 為服務中的每個方法調用進行調用耗時記錄.

  2. 將方法調用的時間戳, 方法名, 調用耗時上報到監控平臺

有了需求, 自然設計實現就很簡單了. 首先我們可以使用 around advice, 然后在方法調用前, 記錄一下開始時間, 然后在方法調用結束后, 記錄結束時間, 它們的時間差就是方法的調用耗時.

我們來看一下具體的 aspect 實現:

ExpiredAopAdviseDefine.java:

@Component
@Aspect
public class ExpiredAopAdviseDefine {private Logger logger = LoggerFactory.getLogger(getClass());// 定義一個 Pointcut, 使用 切點表達式函數 來描述對哪些 Join point 使用 advise.@Pointcut("within(SomeService)")public void pointcut() {}// 定義 advise// 定義 advise@Around("pointcut()")public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {StopWatch stopWatch = new StopWatch();stopWatch.start();// 開始Object retVal = pjp.proceed();stopWatch.stop();// 結束// 上報到公司監控平臺reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());return retVal;}public void reportToMonitorSystem(String methodName, long expiredTime) {logger.info("---method {} invoked, expired time: {} ms---", methodName, expiredTime);//}
}

aspect 一開始定義了一個 pointcut, 匹配 SomeService 類下的所有的方法.
接著呢, 定義了一個 around advice:

@Around("pointcut()")
public Object methodInvokeExpiredTime(ProceedingJoinPoint pjp) throws Throwable {StopWatch stopWatch = new StopWatch();stopWatch.start();// 開始Object retVal = pjp.proceed();stopWatch.stop();// 結束// 上報到公司監控平臺reportToMonitorSystem(pjp.getSignature().toShortString(), stopWatch.getTotalTimeMillis());return retVal;
}

advice 中的代碼也很簡單, 它使用了 Spring 提供的 StopWatch 來統計一段代碼的執行時間. 首先我們先調用 stopWatch.start() 開始計時, 然后通過 pjp.proceed() 來調用我們實際的服務方法, 當調用結束后, 通過 stopWatch.stop() 來結束計時.

接著我們來寫一個簡單的服務, 這個服務提供一個 someMethod 方法用于模擬一個耗時的方法調用:
SomeService.java:

@Service
public class SomeService {private Logger logger = LoggerFactory.getLogger(getClass());private Random random = new Random(System.currentTimeMillis());public void someMethod() {logger.info("---SomeService: someMethod invoked---");try {// 模擬耗時任務Thread.sleep(random.nextInt(500));} catch (InterruptedException e) {e.printStackTrace();}}
}

這樣當 SomeService 類下的方法調用時, 我們所提供的 advice 就會被執行, 因此就可以自動地為我們統計此方法的調用耗時, 并自動上報到監控系統中了.
看到 AOP 的威力了吧, 我們這里僅僅使用了寥寥數語就把一個需求完美地解決了, 并且還與原來的業務邏輯完全解耦, 擴展及其方便.

完整源碼

方法耗時統計

總結

通過上面的幾個簡單例子, 我們對 Spring AOP 的使用應該有了一個更為深入的了解了. 其實 Spring AOP 的使用的地方不止這些, 例如 Spring 的 聲明式事務 就是在 AOP 之上構建的. 讀者朋友也可以根據自己的實際業務場景, 合理使用 Spring AOP, 發揮它的強大功能!

End.

轉載于:https://www.cnblogs.com/root429/p/9251395.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/255865.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/255865.shtml
英文地址,請注明出處:http://en.pswp.cn/news/255865.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

VC Ws2_32.lib

該庫對應WS2_32.DLL,提供了對以下網絡相關API的支持,若使用其中的API,則應該將ws2_32.lib加入工程(否則要動態載入WS2_32.DLL)。acceptbindcloseSOCKETconnectgetpeernamegetsocknamegetsockopthtonlhtonsioctlsocketi…

大話設計模式之策略模式

第二章:商場促銷——策略模式 策略模式的定義:策略模式是一種定義一系列算法的方法,從概念上來看,所有這些算法完成的都是相同的工作,知識實現不同,他可以以相同的方式調用所有的算法,減少了各類算法類與使…

【Python學習】——語言風格(變量賦值、深淺拷貝、for循環陷阱)

目錄 1、賦值 2、賦值的分類——引用賦值、值賦值 1) 不可變對象引用賦值——字符串、數值、元組等 2)可變對象引用賦值——列表、集合、字典 3)可變與不可變對象的引用賦值內部分析 4)在py文件中,和作用域有關,如…

underscore.js 頁面數據渲染

1.underscore.js 源碼 // Underscore.js 1.8.3 // http://underscorejs.org // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license.(function() {// …

判斷莊家是否出貨

1. 大盤處于強勢的時候 日平均線在橫盤的時候,緩慢拉升然后急劇下跌 高位盤整的時候 2. 有利好消息發布的時候 因為莊家會利用這個對于散戶來說這個買入時機來進行出貨操作,可見莊家真是陰險狡詐轉載于:https://www.cnblogs.com/dcz1001/p/6115893.html

【深度學習】——常見深度學習模型總結、anchor-free和anchor-based

目錄 1、faster rcnn: 2、SSD: 3、YOLOv1: 小結: 拓展:anchor-based和anchor-free anchor 1、faster rcnn: FasterRcnn 算法原理講解筆記(非常詳細)https://blog.csdn.net/xjtdw/article…

PHP PDO函數庫詳解

PDO是一個“數據庫訪問抽象層”,作用是統一各種數據庫的訪問接口,與mysql和mysqli的函數庫相比,PDO讓跨數據庫的使用更具有親和力;與ADODB和MDB2相比,PDO更高效。目前而言,實現“數據庫抽象層”任重而道遠&…

數據交互相關分享

Python與web Python Web.py與AJAX交互轉載于:https://juejin.im/post/5a40af3d6fb9a044ff31b1f5

springMVC 相對于 Structs 的優勢

智者說,沒有經過自己的思考和估量,就不能接受別人的東西。資料只能是一個參考,至于是否正確,還得自己去分辨 SpringMVC相對于Structs的幾個優勢: 1、springMVC安全性更高,structs2框架是類級別的攔截&#…

YOLOV1學習

YOLOV1學習(輸入的圖像固定大小為448X448X3) 參考文獻 模型結構 將輸入的圖像歸一化為大小為448x448x3的圖像,然后將經過中間24層的卷積后得到了7x7x1024的特征圖,然后后面連接的是兩個全連接層,分別是4096和1470&am…

KUKA通信 CREAD問題

嗨。 我想通過串行端口1發送X,Y,Z,A,B,C坐標給機器人。 G1: ...... CREAD(HANDLE,SR_T,MR_T,TIMEOUT,OFFSET,"%F",X) P.XX CREAD(HANDLE,SR_T,MR_T,TIMEOUT,OFFSET,"%F",Y) P.YY ...... GOTO G1…

bzoj 1901: Zju2112 Dynamic Rankings

Time Limit: 10 Sec Memory Limit: 128 MBSubmit: 6245 Solved: 2593[Submit][Status][Discuss]Description 給定一個含有n個數的序列a[1],a[2],a[3]……a[n],程序必須回答這樣的詢問:對于給定的i,j,k,在a[i],a[i1],a[i2]……a[j]中第k小的…

第 36 章 RRDTool

36.1. install $ apt-get install rrdtool原文出處:Netkiller 系列 手札 本文作者:陳景峯 轉載請與作者聯系,同時請務必標明文章原始出處和作者信息及本聲明。

手機號碼已經注冊寫到數據庫中,如何利用相同手機號碼再次注冊?

手機號碼已經注冊寫到數據庫中,如何利用相同手機號碼再次注冊? 解:刪除數據庫中以前注冊的手機號碼就可以了啊,delete那條記錄,轉載于:https://www.cnblogs.com/panxuejun/p/6122499.html

騰訊技術研究類和數據分析第一次筆試(2021.8.22)——Python

第一題:開鎖——數學期望 # 最優策略:鑰匙的選擇先從消耗時間最少的開始選擇,然后選擇第二小的依次類推 # 開鎖概率1/n def openLockTime(n, m, time):time_reverse [] # (n,m)->(m,n)for i in range(m):m_time []for j in range(n):m…

教你怎樣選擇伺服電機控制方式

伺服電機一般都有三種控制方式:速度控制方式,轉矩控制方式,位置控制方式 。 速度控制和轉矩控制都是用模擬量來控制的。位置控制是通過發脈沖來控制的。具體采用什么控制方式要根據客戶的要求,滿足何種運動功能來選擇。 …

.Net Discovery系列之四 深入理解.Net垃圾收集機制(下)

上一節給大家介紹了 .Net GC的運行機制,下面來講下與GC相關的重要方法。 第二節.GC關鍵方法解析 1.Dispose()方法 Dispose可用于釋放所有資源,包括托管的和非托管的,需要自己實現。 大多數的非托管資源都要求手動釋放,…

真靜態和偽靜態的區別

首先肯定的是純靜態和偽靜態都是SEO的產物,但純靜態和偽靜態還是有很大區別的。 純靜態是生成真實的HTML頁面保存到服務器端,用戶訪問時直接訪問這 個HTML頁面即可,從而大大的減輕了服務器壓力(如dedecms就是采用的純靜態&#xf…

非常有趣的Console

console覺醒之路,打印個動畫如何? 原文地址: http://www.helloweba.com/view-blog-383.html 批量去掉或替換文本中的換行符(notepad、sublime text2) 原文地址:http://m.blog.csdn.net/article/details?id43228729 有…

shopee蝦皮科技測試工程師第一次筆試

10道單選題 10道多選題 2道編程題 第一題:十進制轉二進制計算1的個數(負數轉為補碼) #!/usr/bin/env python # -*- coding: utf-8 -*- # Time : 2021/8/23 15:44 # Author : linlianqin # Site : # File : 十進制轉換為二進制&am…