javascript
Spring AOP 实战运用
Spring AOP 實(shí)戰(zhàn)
看了上面這么多的理論知識(shí), 不知道大家有沒(méi)有覺(jué)得枯燥哈. 不過(guò)不要急, 俗話(huà)說(shuō)理論是實(shí)踐的基礎(chǔ), 對(duì) Spring AOP 有了基本的理論認(rèn)識(shí)后, 我們來(lái)看一下下面幾個(gè)具體的例子吧.下面的幾個(gè)例子是我在工作中所遇見(jiàn)的比較常用的 Spring AOP 的使用場(chǎng)景, 我精簡(jiǎn)了很多有干擾我們學(xué)習(xí)的注意力的細(xì)枝末節(jié), 以力求整個(gè)例子的簡(jiǎn)潔性.
下面幾個(gè) Demo 的源碼都可以在我的 Github 上下載到.
HTTP 接口鑒權(quán)
首先讓我們來(lái)想象一下如下場(chǎng)景: 我們需要提供的 HTTP RESTful 服務(wù), 這個(gè)服務(wù)會(huì)提供一些比較敏感的信息, 因此對(duì)于某些接口的調(diào)用會(huì)進(jìn)行調(diào)用方權(quán)限的校驗(yàn), 而某些不太敏感的接口則不設(shè)置權(quán)限, 或所需要的權(quán)限比較低(例如某些監(jiān)控接口, 服務(wù)狀態(tài)接口等).
實(shí)現(xiàn)這樣的需求的方法有很多, 例如我們可以在每個(gè) HTTP 接口方法中對(duì)服務(wù)請(qǐng)求的調(diào)用方進(jìn)行權(quán)限的檢查, 當(dāng)調(diào)用方權(quán)限不符時(shí), 方法返回錯(cuò)誤. 當(dāng)然這樣做并無(wú)不可, 不過(guò)如果我們的 api 接口很多, 每個(gè)接口都進(jìn)行這樣的判斷, 無(wú)疑有很多冗余的代碼, 并且很有可能有某個(gè)粗心的家伙忘記了對(duì)調(diào)用者的權(quán)限進(jìn)行驗(yàn)證, 這樣就會(huì)造成潛在的 bug.
那么除了上面的所說(shuō)的方法外, 還有沒(méi)有別的比較優(yōu)雅的方式來(lái)實(shí)現(xiàn)呢? 當(dāng)然有啦, 不然我在這啰嗦半天干嘛呢, 它就是我們今天的主角: AOP.
讓我們來(lái)提煉一下我們的需求:
可以定制地為某些指定的 HTTP RESTful api 提供權(quán)限驗(yàn)證功能.
當(dāng)調(diào)用方的權(quán)限不符時(shí), 返回錯(cuò)誤.
根據(jù)上面所提出的需求, 我們可以進(jìn)行如下設(shè)計(jì):
提供一個(gè)特殊的注解 AuthChecker, 這個(gè)是一個(gè)方法注解, 有此注解所標(biāo)注的 Controller 需要進(jìn)行調(diào)用方權(quán)限的認(rèn)證.
利用 Spring AOP, 以 @annotation 切點(diǎn)標(biāo)志符來(lái)匹配有注解 AuthChecker 所標(biāo)注的 joinpoint.
在 advice 中, 簡(jiǎn)單地檢查調(diào)用者請(qǐng)求中的 Cookie 中是否有我們指定的 token, 如果有, 則認(rèn)為此調(diào)用者權(quán)限合法, 允許調(diào)用, 反之權(quán)限不合法, 范圍錯(cuò)誤.
根據(jù)上面的設(shè)計(jì), 我們來(lái)看一下具體的源碼吧.
首先是 AuthChecker 注解的定義:
AuthChecker.java:
AuthChecker 注解是一個(gè)方法注解, 它用于注解 RequestMapping 方法.
有了注解的定義, 那我們?cè)賮?lái)看一下 aspect 的實(shí)現(xiàn)吧:
HttpAopAdviseDefine.java:
在這個(gè) aspect 中, 我們首先定義了一個(gè) pointcut, 以 @annotation 切點(diǎn)標(biāo)志符來(lái)匹配有注解 AuthChecker 所標(biāo)注的 joinpoint, 即:
// 定義一個(gè) Pointcut, 使用 切點(diǎn)表達(dá)式函數(shù) 來(lái)描述對(duì)哪些 Join point 使用 advise. @Pointcut("@annotation(com.xys.demo1.AuthChecker)") public void pointcut() { }然后再定義一個(gè) advice:
// 定義 advise @Around("pointcut()") public Object checkAuth(ProceedingJoinPoint joinPoint) throws Throwable {HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();// 檢查用戶(hù)所傳遞的 token 是否合法String token = getUserToken(request);if (!token.equalsIgnoreCase("123456")) {return "錯(cuò)誤, 權(quán)限不合法!";}return joinPoint.proceed(); }當(dāng)被 AuthChecker 注解所標(biāo)注的方法調(diào)用前, 會(huì)執(zhí)行我們的這個(gè) advice, 而這個(gè) advice 的處理邏輯很簡(jiǎn)單, 即從 HTTP 請(qǐng)求中獲取名為 user_token 的 cookie 的值, 如果它的值是 123456, 則我們認(rèn)為此 HTTP 請(qǐng)求合法, 進(jìn)而調(diào)用 joinPoint.proceed() 將 HTTP 請(qǐng)求轉(zhuǎn)交給相應(yīng)的控制器處理; 而如果user_token cookie 的值不是 123456, 或?yàn)榭? 則認(rèn)為此 HTTP 請(qǐng)求非法, 返回錯(cuò)誤.
接下來(lái)我們來(lái)寫(xiě)一個(gè)模擬的 HTTP 接口:
DemoController.java:
注意到上面我們提供了兩個(gè) HTTP 接口, 其中 接口 /aop/http/alive 是沒(méi)有 AuthChecker 標(biāo)注的, 而 /aop/http/user_info 接口則用到了 @AuthChecker 標(biāo)注. 那么自然地, 當(dāng)請(qǐng)求了 /aop/http/user_info 接口時(shí), 就會(huì)觸發(fā)我們所設(shè)置的權(quán)限校驗(yàn)邏輯.
接下來(lái)我們來(lái)驗(yàn)證一下, 我們所實(shí)現(xiàn)的功能是否有效吧.
首先在 Postman 中, 調(diào)用 /aop/http/alive 接口, 請(qǐng)求頭中不加任何參數(shù):
可以看到, 我們的 HTTP 請(qǐng)求完全沒(méi)問(wèn)題.
那么再來(lái)看一下請(qǐng)求 /aop/http/user_info 接口會(huì)怎樣呢:
當(dāng)我們請(qǐng)求 /aop/http/user_info 接口時(shí), 服務(wù)返回一個(gè)權(quán)限異常的錯(cuò)誤, 為什么會(huì)這樣呢? 自然就是我們的權(quán)限認(rèn)證系統(tǒng)起了作為: 當(dāng)一個(gè)方法被調(diào)用并且這個(gè)方法有 AuthChecker 標(biāo)注時(shí), 那么首先會(huì)執(zhí)行到我們的 around advice, 在這個(gè) advice 中, 我們會(huì)校驗(yàn) HTTP 請(qǐng)求的 cookie 字段中是否有攜帶 user_token 字段時(shí), 如果沒(méi)有, 則返回權(quán)限錯(cuò)誤.
那么為了能夠正常地調(diào)用 /aop/http/user_info 接口, 我們可以在 Cookie 中添加 user_token=123456, 這樣我們可以愉快的玩耍了:
注意, Postman 默認(rèn)是不支持 Cookie 的, 所以為了實(shí)現(xiàn)添加 Cookie 的功能, 我們需要安裝 Postman 的 interceptor 插件. 安裝方法可以看官網(wǎng)的文章
完整源碼
HTTP 接口鑒權(quán)
方法調(diào)用日志
第二個(gè) AOP 實(shí)例是記錄一個(gè)方法調(diào)用的log. 這應(yīng)該是一個(gè)很常見(jiàn)的功能了.首先假設(shè)我們有如下需求:
某個(gè)服務(wù)下的方法的調(diào)用需要有 log: 記錄調(diào)用的參數(shù)以及返回結(jié)果.
當(dāng)方法調(diào)用出異常時(shí), 有特殊處理, 例如打印異常 log, 報(bào)警等.
根據(jù)上面的需求, 我們可以使用 before advice 來(lái)在調(diào)用方法前打印調(diào)用的參數(shù), 使用 after returning advice 在方法返回打印返回的結(jié)果. 而當(dāng)方法調(diào)用失敗后, 可以使用 after throwing advice 來(lái)做相應(yīng)的處理.那么我們來(lái)看一下 aspect 的實(shí)現(xiàn):
@Component @Aspect public class LogAopAdviseDefine {private Logger logger = LoggerFactory.getLogger(getClass());// 定義一個(gè) Pointcut, 使用 切點(diǎn)表達(dá)式函數(shù) 來(lái)描述對(duì)哪些 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());} }第一步, 自然是定義一個(gè) pointcut, 以 within 切點(diǎn)標(biāo)志符來(lái)匹配類(lèi) NeedLogService 下的所有 joinpoint, 即:
@Pointcut("within(NeedLogService)") public void pointcut() { }接下來(lái)根據(jù)我們前面的設(shè)計(jì), 我們分別定義了三個(gè) advice, 第一個(gè)是一個(gè) before advice:
@Before("pointcut()") public void logMethodInvokeParam(JoinPoint joinPoint) {logger.info("---Before method {} invoke, param: {}---", joinPoint.getSignature().toShortString(), joinPoint.getArgs()); }它在一個(gè)符合要求的 joinpoint 方法調(diào)用前執(zhí)行, 打印調(diào)用的方法名和調(diào)用的參數(shù).
第二個(gè)是 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()); }這個(gè) advice 會(huì)在方法調(diào)用成功后打印出方法名還反的參數(shù).
最后一個(gè)是 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()); }這個(gè) advice 會(huì)在指定的 joinpoint 拋出異常時(shí)執(zhí)行, 打印異常的信息.
接下來(lái)我們?cè)賹?xiě)兩個(gè) Service 類(lèi):
NeedLogService.java:
NormalService.java:
@Service public class NormalService {private Logger logger = LoggerFactory.getLogger(getClass());public void someMethod() {logger.info("---NormalService: someMethod invoked---");} }根據(jù)我們 pointcut 的規(guī)則, 類(lèi) NeedLogService 下的所有方法都會(huì)被織入 advice, 而類(lèi) NormalService 則不會(huì).
最后我們分別調(diào)用這幾個(gè)方法:
@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---根據(jù) log, 我們知道, NeedLogService.logMethod 執(zhí)行的前后確實(shí)有 advice 執(zhí)行了, 并且在 NeedLogService.exceptionMethod 拋出異常后, logMethodInvokeException 這個(gè) advice 也被執(zhí)行了. 而由于 pointcut 的匹配規(guī)則, 在 NormalService 類(lèi)中的方法則不會(huì)織入 advice.
完整源碼
方法調(diào)用日志
方法耗時(shí)統(tǒng)計(jì)
作為程序員, 我們都知道服務(wù)監(jiān)控對(duì)于一個(gè)服務(wù)能夠長(zhǎng)期穩(wěn)定運(yùn)行的重要性, 因此很多公司都有自己內(nèi)部的監(jiān)控報(bào)警系統(tǒng), 或者是使用一些開(kāi)源的系統(tǒng), 例如小米的 Falcon 監(jiān)控系統(tǒng).
那么在程序監(jiān)控中, AOP 有哪些用武之地呢? 我們來(lái)假想一下如下場(chǎng)景:
有一天, leader 對(duì)小王說(shuō), "小王啊, 你負(fù)責(zé)的那個(gè)服務(wù)不太穩(wěn)定啊, 經(jīng)常有超時(shí)發(fā)生! 你有對(duì)這些服務(wù)接口進(jìn)行過(guò)耗時(shí)統(tǒng)計(jì)嗎?"
耗時(shí)統(tǒng)計(jì)? 小王嘀咕了, 小聲的回答到: "還沒(méi)有加呢."
leader: "你看著辦吧, 我明天要看到各個(gè)時(shí)段的服務(wù)接口調(diào)用的耗時(shí)分布!"
小王這就犯難了, 雖然說(shuō)計(jì)算一個(gè)方法的調(diào)用耗時(shí)并不是一個(gè)很難的事情, 但是整個(gè)服務(wù)有二十來(lái)個(gè)接口呢, 一個(gè)一個(gè)地添加統(tǒng)計(jì)代碼, 那還不是要累死人了.
看著同事一個(gè)一個(gè)都下班回家了, 小王眉頭更加緊了. 不過(guò)此時(shí)小王靈機(jī)一動(dòng): "噫, 有了!".
小王想到了一個(gè)好方法, 立即動(dòng)手, 吭哧吭哧地幾分鐘就搞定了.
那么小王的解決方法是什么呢? 自然是我們的主角 AOP 啦.
首先讓我們來(lái)提煉一下需求:
為服務(wù)中的每個(gè)方法調(diào)用進(jìn)行調(diào)用耗時(shí)記錄.
將方法調(diào)用的時(shí)間戳, 方法名, 調(diào)用耗時(shí)上報(bào)到監(jiān)控平臺(tái)
有了需求, 自然設(shè)計(jì)實(shí)現(xiàn)就很簡(jiǎn)單了. 首先我們可以使用 around advice, 然后在方法調(diào)用前, 記錄一下開(kāi)始時(shí)間, 然后在方法調(diào)用結(jié)束后, 記錄結(jié)束時(shí)間, 它們的時(shí)間差就是方法的調(diào)用耗時(shí).
我們來(lái)看一下具體的 aspect 實(shí)現(xiàn):
ExpiredAopAdviseDefine.java:
@Component @Aspect public class ExpiredAopAdviseDefine {private Logger logger = LoggerFactory.getLogger(getClass());// 定義一個(gè) Pointcut, 使用 切點(diǎn)表達(dá)式函數(shù) 來(lái)描述對(duì)哪些 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();// 開(kāi)始Object retVal = pjp.proceed();stopWatch.stop();// 結(jié)束// 上報(bào)到公司監(jiān)控平臺(tái)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 一開(kāi)始定義了一個(gè) pointcut, 匹配 SomeService 類(lèi)下的所有的方法.
接著呢, 定義了一個(gè) around advice:
advice 中的代碼也很簡(jiǎn)單, 它使用了 Spring 提供的 StopWatch 來(lái)統(tǒng)計(jì)一段代碼的執(zhí)行時(shí)間. 首先我們先調(diào)用 stopWatch.start() 開(kāi)始計(jì)時(shí), 然后通過(guò) pjp.proceed() 來(lái)調(diào)用我們實(shí)際的服務(wù)方法, 當(dāng)調(diào)用結(jié)束后, 通過(guò) stopWatch.stop() 來(lái)結(jié)束計(jì)時(shí).
接著我們來(lái)寫(xiě)一個(gè)簡(jiǎn)單的服務(wù), 這個(gè)服務(wù)提供一個(gè) someMethod 方法用于模擬一個(gè)耗時(shí)的方法調(diào)用:
SomeService.java:
這樣當(dāng) SomeService 類(lèi)下的方法調(diào)用時(shí), 我們所提供的 advice 就會(huì)被執(zhí)行, 因此就可以自動(dòng)地為我們統(tǒng)計(jì)此方法的調(diào)用耗時(shí), 并自動(dòng)上報(bào)到監(jiān)控系統(tǒng)中了.
看到 AOP 的威力了吧, 我們這里僅僅使用了寥寥數(shù)語(yǔ)就把一個(gè)需求完美地解決了, 并且還與原來(lái)的業(yè)務(wù)邏輯完全解耦, 擴(kuò)展及其方便.
完整源碼
方法耗時(shí)統(tǒng)計(jì)
總結(jié)
通過(guò)上面的幾個(gè)簡(jiǎn)單例子, 我們對(duì) Spring AOP 的使用應(yīng)該有了一個(gè)更為深入的了解了. 其實(shí) Spring AOP 的使用的地方不止這些, 例如 Spring 的 聲明式事務(wù) 就是在 AOP 之上構(gòu)建的. 讀者朋友也可以根據(jù)自己的實(shí)際業(yè)務(wù)場(chǎng)景, 合理使用 Spring AOP, 發(fā)揮它的強(qiáng)大功能!
End.
轉(zhuǎn)載于:https://www.cnblogs.com/root429/p/9251395.html
總結(jié)
以上是生活随笔為你收集整理的Spring AOP 实战运用的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 下班了接着上班 国产硬核《会计模拟器》上
- 下一篇: 梦回JavaScript--数据类型之u