AOP概念html
AOP 的全稱爲 Aspect Oriented Programming,譯爲面向切面編程。實際上 AOP 就是經過預編譯和運行期動態代理實現程序功能的統一維護的一種技術。在不一樣的技術棧中 AOP 有着不一樣的實現,可是其做用都相差不遠,咱們經過 AOP 爲既有的程序定義一個切入點,而後在切入點先後插入不一樣的執行內容,以達到在不修改原有代碼業務邏輯的前提下統一處理一些內容(好比日誌處理、分佈式鎖)的目的。前端
爲何要使用 AOPjava
在實際的開發過程當中,咱們的應用程序會被分爲不少層。一般來說一個 Java 的 Web 程序會擁有如下幾個層次:web
雖然看起來每一層都作着全然不一樣的事情,可是實際上總會有一些相似的代碼,好比日誌打印和安全驗證等等相關的代碼。若是咱們選擇在每一層都獨立編寫這部分代碼,那麼長此以往代碼將變的很難維護。因此咱們提供了另外的一種解決方案: AOP。這樣能夠保證這些通用的代碼被聚合在一塊兒維護,並且咱們能夠靈活的選擇何處須要使用這些代碼。redis
AOP 的核心概念spring
Spring AOP數據庫
Spring 中的 AOP 代理仍是離不開 Spring 的 IOC 容器,代理的生成,管理及其依賴關係都是由 IOC 容器負責,Spring 默認使用 JDK 動態代理,在須要代理類而不是代理接口的時候,Spring 會自動切換爲使用 CGLIB 代理,不過如今的項目都是面向接口編程,因此 JDK 動態代理相對來講用的仍是多一些。在本文中,咱們將以註解結合 AOP 的方式來分別實現 Web 日誌處理和分佈式鎖。編程
Spring AOP 相關注解緩存
其中 @Before 、 @After 、 @AfterReturning 、 @Around 、 @AfterThrowing 都屬於通知。安全
AOP 順序問題
在實際狀況下,咱們對同一個接口作多個切面,好比日誌打印、分佈式鎖、權限校驗等等。這時候咱們就會面臨一個優先級的問題,這麼多的切面該如何告知 Spring 執行順序呢?這就須要咱們定義每一個切面的優先級,咱們可使用 @Order(i) 註解來標識切面的優先級, i 的值越小,優先級越高。假設如今咱們一共有兩個切面,一個 WebLogAspect ,咱們爲其設置 @Order(100) ;而另一個切面 DistributeLockAspect 設置爲 @Order(99) ,因此 DistributeLockAspect 有更高的優先級,這個時候執行順序是這樣的:在 @Before 中優先執行 @Order(99) 的內容,再執行 @Order(100) 的內容。而在 @After 和 @AfterReturning 中則優先執行 @Order(100) 的內容,再執行 @Order(99) 的內容,能夠理解爲先進後出的原則。
基於註解的 AOP 配置
使用註解一方面能夠減小咱們的配置,另外一方面註解在編譯期間就能夠驗證正確性,查錯相對比較容易,並且配置起來也至關方便。相信你們也都有所瞭解,咱們如今的 Spring 項目裏面使用了很是多的註解替代了以前的 xml 配置。而將註解與 AOP 配合使用也是咱們最經常使用的方式,在本文中咱們將以這種模式實現 Web 日誌統一處理和分佈式鎖兩個註解。下面就讓咱們從準備工做開始吧。
準備工做
準備一個 Spring Boot 的 Web 項目
你能夠經過
添加依賴
咱們須要添加 Web 依賴和 AOP 相關依賴,只須要在 pom.xml 中添加以下內容便可:
清單 1. 添加 web 依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId></dependency>複製代碼
清單 2. 添加 AOP 相關依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId></dependency>複製代碼
其餘準備工做
爲了方便測試我還在項目中集成了 Swagger 文檔,具體的集成方法能夠參照
利用 AOP 實現 Web 日誌處理
爲何要實現 Web 日誌統一處理
在實際的開發過程當中,咱們會須要將接口的出請求參數、返回數據甚至接口的消耗時間都以日誌的形式打印出來以便排查問題,有些比較重要的接口甚至還須要將這些信息寫入到數據庫。而這部分代碼相對來說比較類似,爲了提升代碼的複用率,咱們能夠以 AOP 的方式將這種相似的代碼封裝起來。
Web 日誌註解
清單 3. Web 日誌註解代碼
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface ControllerWebLog { String name(); boolean intoDb() default false;}複製代碼
其中 name 爲所調用接口的名稱, intoDb 則標識該條操做日誌是否須要持久化存儲,Spring Boot 鏈接數據庫的配置,能夠參考
實現 WebLogAspect 切面
@Aspect@Component@Order(100)public class WebLogAspect {}複製代碼
@Pointcut("execution(* cn.itweknow.sbaop.controller..*.*(..))")public void webLog() {}複製代碼
execution(<修飾符模式>?<返回類型模式><方法名模式>(<參數模式>)<異常模式>?)複製代碼
@Before(value = "webLog()& & @annotation(controllerWebLog)") public void doBefore(JoinPoint joinPoint, ControllerWebLog controllerWebLog) { // 開始時間。 long startTime = System.currentTimeMillis(); Map<String, Object> threadInfo = new HashMap<>(); threadInfo.put(START_TIME, startTime); // 請求參數。 StringBuilder requestStr = new StringBuilder(); Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { for (Object arg : args) { requestStr.append(arg.toString()); } } threadInfo.put(REQUEST_PARAMS, requestStr.toString()); threadLocal.set(threadInfo); logger.info("{}接口開始調用:requestData={}", controllerWebLog.name(), threadInfo.get(REQUEST_PARAMS)); }複製代碼
@AfterReturning(value = "webLog()&& @annotation(controllerWebLog)", returning = "res")public void doAfterReturning(ControllerWebLog controllerWebLog, Object res) { Map<String, Object> threadInfo = threadLocal.get(); long takeTime = System.currentTimeMillis() - (long) threadInfo.getOrDefault(START_TIME, System.currentTimeMillis()); if (controllerWebLog.intoDb()) { insertResult(controllerWebLog.name(), (String) threadInfo.getOrDefault(REQUEST_PARAMS, ""), JSON.toJSONString(res), takeTime); } threadLocal.remove(); logger.info("{}接口結束調用:耗時={}ms,result={}", controllerWebLog.name(), takeTime, res);}複製代碼
@AfterThrowing(value = "webLog()& & @annotation(controllerWebLog)", throwing = "throwable") public void doAfterThrowing(ControllerWebLog controllerWebLog, Throwable throwable) { Map< String, Object> threadInfo = threadLocal.get(); if (controllerWebLog.intoDb()) { insertError(controllerWebLog.name(), (String)threadInfo.getOrDefault(REQUEST_PARAMS, ""), throwable); } threadLocal.remove(); logger.error("{}接口調用異常,異常信息{}",controllerWebLog.name(), throwable);}複製代碼
@PostMapping("/post-test")@ApiOperation("接口日誌 POST 請求測試")@ControllerWebLog(name = "接口日誌 POST 請求測試", intoDb = true)public BaseResponse postTest(@RequestBody BaseRequest baseRequest) {}複製代碼
利用 AOP 實現分佈式鎖
爲何要使用分佈式鎖
咱們程序中多多少少會有一些共享的資源或者數據,在某些時候咱們須要保證同一時間只能有一個線程訪問或者操做它們。在傳統的單機部署的狀況下,咱們簡單的使用 Java 提供的併發相關的 API 處理便可。可是如今大多數服務都採用分佈式的部署方式,咱們就須要提供一個跨進程的互斥機制來控制共享資源的訪問,這種互斥機制就是咱們所說的分佈式鎖。
注意
分佈式鎖註解
清單 11. 分佈式鎖註解
@Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public @interface DistributeLock { String key(); long timeout() default 5; TimeUnit timeUnit() default TimeUnit.SECONDS;}複製代碼
其中, key 爲分佈式所的 key 值, timeout 爲鎖的超時時間,默認爲 5, timeUnit 爲超時時間的單位,默認爲秒。
註解參數解析器
因爲註解屬性在指定的時候只能爲常量,咱們沒法直接使用方法的參數。而在絕大多數的狀況下分佈式鎖的 key 值是須要包含方法的一個或者多個參數的,這就須要咱們將這些參數的位置以某種特殊的字符串表示出來,而後經過參數解析器去動態的解析出來這些參數具體的值,而後拼接到 key 上。在本教程中我也編寫了一個參數解析器 AnnotationResolver 。篇幅緣由,其源碼就不直接粘在文中,須要的讀者能夠
獲取鎖方法
清單 12. 獲取鎖
private String getLock(String key, long timeout, TimeUnit timeUnit) { try { String value = UUID.randomUUID().toString(); Boolean lockStat = stringRedisTemplate.execute((RedisCallback< Boolean>)connection -> connection.set(key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")), Expiration.from(timeout, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT)); if (!lockStat) { // 獲取鎖失敗。 return null; } return value; } catch (Exception e) { logger.error("獲取分佈式鎖失敗,key={}", key, e); return null; }}複製代碼
RedisStringCommands.SetOption.SET_IF_ABSENT 其實是使用了 setNX 命令,若是 key 已經存在的話則不進行任何操做返回失敗,若是 key 不存在的話則保存 key 並返回成功,該命令在成功的時候返回 1,結束的時候返回 0。咱們隨機產生了一個 value 而且在獲取鎖成功的時候返回出去,是爲了在釋放鎖的時候對該值進行比較,這樣能夠作到解鈴還須繫鈴人,由誰建立的鎖就由誰釋放。同時還指定了超時時間,這樣能夠保證鎖釋放失敗的狀況下不會形成接口永遠不能訪問。
釋放鎖方法
清單 13. 釋放鎖
private void unLock(String key, String value) { try { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; boolean unLockStat = stringRedisTemplate.execute((RedisCallback< Boolean>)connection -> connection.eval(script.getBytes(), ReturnType.BOOLEAN, 1, key.getBytes(Charset.forName("UTF-8")), value.getBytes(Charset.forName("UTF-8")))); if (!unLockStat) { logger.error("釋放分佈式鎖失敗,key={},已自動超時,其餘線程可能已經從新獲取鎖", key); } } catch (Exception e) { logger.error("釋放分佈式鎖失敗,key={}", key, e); }}複製代碼
切面
切點和 Web 日誌處理的切點同樣,這裏再也不贅述。咱們在切面中使用的通知類型爲 @Around,在切點以前咱們先嚐試獲取鎖,若獲取鎖失敗則直接返回錯誤信息,若獲取鎖成功則執行方法體,當方法結束後(不管是正常結束仍是異常終止)釋放鎖。
清單 14. 環繞通知
@Around(value = "distribute()&& @annotation(distributeLock)")public Object doAround(ProceedingJoinPoint joinPoint, DistributeLock distributeLock) throws Exception { String key = annotationResolver.resolver(joinPoint, distributeLock.key()); String keyValue = getLock(key, distributeLock.timeout(), distributeLock.timeUnit()); if (StringUtil.isNullOrEmpty(keyValue)) { // 獲取鎖失敗。 return BaseResponse.addError(ErrorCodeEnum.OPERATE_FAILED, "請勿頻繁操做"); } // 獲取鎖成功 try { return joinPoint.proceed(); } catch (Throwable throwable) { return BaseResponse.addError(ErrorCodeEnum.SYSTEM_ERROR, "系統異常"); } finally { // 釋放鎖。 unLock(key, keyValue); }}複製代碼
測試
清單 15. 分佈式鎖測試代碼
@PostMapping("/post-test")@ApiOperation("接口日誌 POST 請求測試")@ControllerWebLog(name = "接口日誌 POST 請求測試", intoDb = true)@DistributeLock(key = "post_test_#{baseRequest.channel}", timeout = 10)public BaseResponse postTest(@RequestBody BaseRequest baseRequest) { try { Thread.sleep(10000); } catch (InterruptedException e) { e.printStackTrace(); } return BaseResponse.addResult();}複製代碼
在本次測試中咱們將鎖的超時時間設置爲 10 秒鐘,在接口中讓當前線程睡眠 10 秒,這樣能夠保證 10 秒鐘以內鎖不會被釋放掉,測試起來更加容易些。啓動項目後,咱們快速訪問兩次該接口,注意兩次請求的 channel 傳值須要一致(由於鎖的 key 中包含該值),會發現第二次訪問時返回以下結果:
圖 2. 基於 Redis 的分佈式鎖測試效果
這就說明咱們的分佈式鎖已經生效。
結束語
在本教程中,咱們主要了解了 AOP 編程以及爲何要使用 AOP。也介紹瞭如何在 Spring Boot 項目中利用 AOP 實現 Web 日誌統一處理和基於 Redis 的分佈式鎖。你能夠在 Github 上找到本教程的
順便給你們推薦一個Java技術交流羣:710373545裏面會分享一些資深架構師錄製的視頻資料:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源,目前受益良多!