接上篇 完全征服 Spring AOP 之 理論篇html
看了上面這麼多的理論知識, 不知道你們有沒有以爲枯燥哈. 不過不要急, 俗話說理論是實踐的基礎, 對 Spring AOP 有了基本的理論認識後, 咱們來看一下下面幾個具體的例子吧.
下面的幾個例子是我在工做中所碰見的比較經常使用的 Spring AOP 的使用場景, 我精簡了不少有干擾咱們學習的注意力的細枝末節, 以力求整個例子的簡潔性.java
下面幾個 Demo 的源碼均可以在個人 Github 上下載到.git
首先讓咱們來想象一下以下場景: 咱們須要提供的 HTTP RESTful 服務, 這個服務會提供一些比較敏感的信息, 所以對於某些接口的調用會進行調用方權限的校驗, 而某些不太敏感的接口則不設置權限, 或所須要的權限比較低(例如某些監控接口, 服務狀態接口等).
實現這樣的需求的方法有不少, 例如咱們能夠在每一個 HTTP 接口方法中對服務請求的調用方進行權限的檢查, 當調用方權限不符時, 方法返回錯誤. 固然這樣作並沒有不可, 不過若是咱們的 api 接口不少, 每一個接口都進行這樣的判斷, 無疑有不少冗餘的代碼, 而且頗有可能有某個粗心的傢伙忘記了對調用者的權限進行驗證, 這樣就會形成潛在的 bug.
那麼除了上面的所說的方法外, 還有沒有別的比較優雅的方式來實現呢? 固然有啦, 否則我在這囉嗦半天干嗎呢, 它就是咱們今天的主角: AOP
.程序員
讓咱們來提煉一下咱們的需求:github
根據上面所提出的需求, 咱們能夠進行以下設計:spring
AuthChecker
, 這個是一個方法註解, 有此註解所標註的 Controller 須要進行調用方權限的認證.AuthChecker
所標註的 joinpoint.根據上面的設計, 咱們來看一下具體的源碼吧.
首先是 AuthChecker
註解的定義:
AuthChecker.java:api
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface AuthChecker { }
AuthChecker
註解是一個方法註解, 它用於註解 RequestMapping 方法.cookie
有了註解的定義, 那咱們再來看一下 aspect 的實現吧:
HttpAopAdviseDefine.java:app
@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, 即:dom
// 定義一個 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
插件. 安裝方法能夠看官網的文章
第二個 AOP 實例是記錄一個方法調用的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
啦.
首先讓咱們來提煉一下需求:
有了需求, 天然設計實現就很簡單了. 首先咱們可使用 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, 發揮它的強大功能!