基于SpringAOP面向切面編程的一些實踐(日志記錄、權限控制、統一異常處理)

前言

Spring框架中的AOP(面向切面編程)

????????通過上面的文章我們了解到了AOP面向切面編程的思想,接下來通過一些實踐,去更加深入的了解我們所學到的知識。


簡單回顧一下AOP的常見應用場景

  • 日志記錄:記錄方法入參、返回值、執行性能等日志信息。

  • 權限控制:通過自定義注解檢查用戶權限,進行基本的權限控制。

  • 統一異常處理:通過捕獲Controller層的異常可以已經統一的異常響應處理。

????????接下來,將對上述場景分別進行實踐。


準備工作

1、基礎依賴

  • JDK17

  • lombok

2、梳理項目結構

aop-demo
├── pom.xml
├── aop-demo-logging
├── aop-demo-permission
└── aop-demo-exception

一、日志記錄

1、梳理一下需要記錄的信息

  • 記錄當前執行方法的線程信息。

  • 記錄方法參數(可選)。

  • 記錄方法返回值(可選)。

  • 記錄方法執行時間。

  • 記錄方法執行是否超出閾值,若超出閾值進行一定提示。

  • 數據脫敏。

2、實現注解

????????實現注解,通過給方法加上注解的方式進行日志記錄。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {/** 是否記錄參數(默認開啟) */boolean logParams() default true;/** 是否記錄返回值(默認開啟) */boolean logResult() default true;/** 超時警告閾值(單位:毫秒) */long warnThreshold() default 1000;
}

3、實現切面類

????????通過環繞通知的方式,記錄方法信息,并收集上面整理的信息。

@Aspect
@Component
@Slf4j
public class LoggingAspect {// 線程信息格式化模板private static final String THREAD_INFO_TEMPLATE = "Thread[ID=%d, Name=%s]";@Pointcut("@annotation(com.djhhh.annotation.Loggable)")public void loggableMethod() {}@Around("loggableMethod()")public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {// 獲取當前線程信息Thread currentThread = Thread.currentThread();String threadInfo = String.format(THREAD_INFO_TEMPLATE,currentThread.getId(),currentThread.getName());MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();String methodName = method.getDeclaringClass().getSimpleName() + "#" + method.getName();Loggable loggable = method.getAnnotation(Loggable.class);boolean logParams = loggable == null || loggable.logParams();boolean logResult = loggable == null || loggable.logResult();long warnThreshold = loggable != null ? loggable.warnThreshold() : 1000;// 記錄開始日志(添加線程信息)if (logParams) {log.info("{} - Method [{}] started with params: {}",threadInfo, methodName, formatParams(joinPoint.getArgs()));} else {log.info("{} - Method [{}] started", threadInfo, methodName);}long start = System.currentTimeMillis();Object result = null;try {result = joinPoint.proceed();return result;} catch (Exception e) {// 異常日志添加線程信息log.error("{} - Method [{}] failed: {} - {}",threadInfo, methodName, e.getClass().getSimpleName(), e.getMessage());throw e;} finally {long duration = System.currentTimeMillis() - start;String durationMsg = String.format("%s - Method [%s] completed in %d ms",threadInfo, methodName, duration);if (duration > warnThreshold) {log.warn("{} (超過閾值{}ms)", durationMsg, warnThreshold);} else {log.info(durationMsg);}if (logResult && result != null) {// 結果日志添加線程信息log.info("{} - Method [{}] result: {}",threadInfo, methodName, formatResult(result));}}}// 參數格式化(保持不變)private String formatParams(Object[] args) {return Arrays.stream(args).map(arg -> {if (arg instanceof String) return "String[****]";if (arg instanceof Password) return "Password[PROTECTED]";return Objects.toString(arg);}).collect(Collectors.joining(", "));}// 結果格式化優化:集合類型顯示大小private String formatResult(Object result) {return result.toString();}
}

4、實現測試服務

????????通過下列的五個的服務進行測試,詳細測試情況看下文。

@Service
@Slf4j
public class TestServiceImpl implements TestService {@Override@Loggablepublic Integer sum(ArrayList<Integer> arr) {return arr.stream().mapToInt(Integer::intValue).sum();}@Override@Loggable(warnThreshold = 5)public Integer sumMx(ArrayList<Integer> arr) {try{Thread.sleep(5000);}catch (Exception e){log.error(e.getMessage());}return arr.stream().mapToInt(Integer::intValue).sum();}@Override@Loggablepublic Boolean login(String username, Password password) {return "djhhh".equals(username)&&"123456".equals(password.getPassword());}@Override@Loggable(logResult = false,logParams = false)public void logout() {log.info("登出成功");}
}

5、測試

@SpringBootTest
@ExtendWith({SpringExtension.class, OutputCaptureExtension.class})
class LoggingAspectTest {@Autowiredprivate TestServiceImpl testService;//---- 測試業務邏輯正確性 ----@Test@DisplayName("測試sum方法-正常計算")void testSum_NormalCalculation() {ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));int result = testService.sum(list);assertEquals(6, result);}@Test@DisplayName("測試login方法-正確憑證")void testLogin_CorrectCredentials() {Password password = new Password("123456");boolean result = testService.login("djhhh", password);assertTrue(result);}@Test@DisplayName("測試login方法-錯誤憑證")void testLogin_WrongCredentials() {Password password = new Password("wrong");boolean result = testService.login("djhhh", password);assertFalse(result);}@Test@DisplayName("測試logout方法-無參數無返回值")void testLogout() {assertDoesNotThrow(() -> testService.logout());}//---- 驗證日志切面功能 ----@Test@DisplayName("驗證sum方法-參數和結果日志")void testSum_Logging(CapturedOutput output) {ArrayList<Integer> list = new ArrayList<>(Arrays.asList(1, 2, 3));testService.sum(list);// 驗證日志內容String logs = output.toString();assertTrue(logs.contains("Method [TestServiceImpl#sum] started with params: [1, 2, 3]"));assertTrue(logs.contains("Method [TestServiceImpl#sum] result: 6"));}@Test@DisplayName("驗證login方法-敏感參數脫敏")void testLogin_SensitiveParamMasking(CapturedOutput output) {Password password = new Password("123456");testService.login("djhhh", password);// 驗證參數脫敏String logs = output.toString();assertTrue(logs.contains("String[****], Password[PROTECTED]"), "未正確脫敏敏感參數");assertFalse(logs.contains("123456"), "密碼明文泄露");}@Test@DisplayName("驗證logout方法-關閉參數和結果日志")void testLogout_NoParamNoResultLog(CapturedOutput output) {testService.logout();String logs = output.toString();assertTrue(logs.contains("Method [TestServiceImpl#logout] started"));assertFalse(logs.contains("started with params"));assertFalse(logs.contains("result:"));}@Test@DisplayName("驗證sumMx方法-超時告警")void testSumMx_ThresholdExceeded(CapturedOutput output) throws InterruptedException {// 構造大數據量延長執行時間(根據實際性能調整)ArrayList<Integer> bigList = new ArrayList<>();for (int i = 0; i < 10; i++) {bigList.add(i);}testService.sumMx(bigList);// 驗證超時警告String logs = output.toString();assertTrue(logs.contains("(超過閾值5ms)"), "未觸發超時警告");}
}

????????測試結果如下:

????????至此通過Spring AOP實現日志記錄的實踐完畢。

實踐總結

????????通過SpringAOP實現日志記錄的解耦,將日志邏輯從業務代碼中剝離,提升了代碼的可維護性和系統運行狀態的可觀測性。


二、權限校驗

????????本實踐只進行基礎的權限身份校驗,想要更加詳細的權限校驗權限可以參考下面的文章。

權限系統設計方案實踐(Spring Security + RBAC 模型)

1、線程工具

????????用于保存用戶信息。

public class UserContext {private static final ThreadLocal<Set<String>> permissionsHolder = new ThreadLocal<>();// 設置當前用戶權限public static void setCurrentPermissions(Set<String> permissions) {permissionsHolder.set(permissions);}// 獲取當前用戶權限public static Set<String> getCurrentPermissions() {return permissionsHolder.get();}// 清除上下文public static void clear() {permissionsHolder.remove();}
}

2、實現注解和常量類

????????實現注解和常量類,為后續權限校驗進行準備工作。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Role {/** 需要的權限標識 */String[] value();/** 校驗邏輯:AND(需全部滿足)或 OR(滿足其一) */Logical logical() default Logical.AND;
}
enum class Logical {AND, OR
}

3、定義切面類

????????通過定義切面類進行權限校驗。

????????通過@Before注解,在進入方法之前進行權限校驗。

@Aspect
@Component
public class PermissionAspect {@Pointcut("@annotation(role)")public void rolePointcut(Role role) {}/*** 定義切入點:攔截所有帶 @RequiresPermission 注解的方法*/@Before("rolePointcut(role)")public void checkPermission(Role role){// 獲取當前用戶權限列表(需自行實現用戶權限獲取邏輯)Set<String> userPermissions = UserContext.getCurrentPermissions();// 校驗權限boolean hasPermission;String[] requiredPermissions = role.value();Logical logical = role.logical();if (logical == Logical.AND) {hasPermission = Arrays.stream(requiredPermissions).allMatch(userPermissions::contains);} else {hasPermission = Arrays.stream(requiredPermissions).anyMatch(userPermissions::contains);}if (!hasPermission) {throw new RuntimeException("權限不足,所需權限: " + Arrays.toString(requiredPermissions));}}
}

4、實現測試服務

????????兩個方法,分別測試滿足權限和不滿足權限。

@Service
public class TestService {@Role(value = {"order:read", "order:write"}, logical = Logical.OR)public void query(Long id) {}@Role("order:admin")public void delete(Long id) {}
}

5、測試

????????對兩種情況分別進行測試。

@SpringBootTest
public class PermissionAspectTest {@Autowiredprivate TestService testService;@Test@DisplayName("測試AND邏輯-權限滿足")void testAndLogicSuccess() {// 模擬用戶有全部權限UserContext.setCurrentPermissions(Set.of("order:read", "order:write"));assertDoesNotThrow(() -> testService.query(1L));}@Test@DisplayName("測試OR邏輯-權限不足")void testOrLogicFailure() {// 模擬用戶只有部分權限UserContext.setCurrentPermissions(Set.of("order:read"));assertThrows(RuntimeException.class,() -> testService.delete(1L),"應檢測到權限不足");}
}

????????測試結果如下:

實踐總結

????????通過AOP可以進行簡單的權限校驗工作,若項目中對權限的顆粒度需求沒有那么細的情況下,可以使用該方法進行權限校驗。


三、異常統一處理

1、準備工作

響應類

@Data
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {private int code;    // 業務狀態碼private String msg;  // 錯誤描述private T data;      // 返回數據// 快速創建成功響應public static <T> ApiResponse<T> success(T data) {return new ApiResponse<>(200, "success", data);}// 快速創建錯誤響應public static ApiResponse<?> error(int code, String msg) {return new ApiResponse<>(code, msg, null);}
}

自定義異常類

@Getter
public class BusinessException extends RuntimeException {private final int code;  // 自定義錯誤碼public BusinessException(int code, String message) {super(message);this.code = code;}
}

2、全局異常捕捉

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {/*** 處理業務異常(返回HTTP 200,通過code區分錯誤)*/@ExceptionHandler(BusinessException.class)public ApiResponse<?> handleBusinessException(BusinessException e) {log.error("業務異常: code={}, msg={}", e.getCode(), e.getMessage());return ApiResponse.error(e.getCode(), e.getMessage());}/*** 處理參數校驗異常(返回HTTP 400)*/@ResponseStatus(HttpStatus.BAD_REQUEST)@ExceptionHandler(BindException.class)public ApiResponse<?> handleValidationException(BindException e) {String errorMsg = e.getBindingResult().getAllErrors().get(0).getDefaultMessage();log.error("參數校驗失敗: {}", errorMsg);return ApiResponse.error(400, errorMsg);}/*** 處理其他所有異常(返回HTTP 500)*/@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)@ExceptionHandler(Exception.class)public ApiResponse<?> handleGlobalException(Exception e) {log.error("系統異常: ", e);return ApiResponse.error(500, "系統繁忙,請稍后重試");}
}

3、測試類

@RestController
@RequestMapping("/api")
public class TestController {@GetMapping("/test/get")public ApiResponse<String> test(@RequestParam String id){if(id==null){throw new RuntimeException("id為空");}return ApiResponse.success(id);}
}

????????測試結果如下:

實踐總結

在項目中比較常用的一個異常捕獲方式,我們可以通過該方式,統一捕獲項目中的異常,便于項目的異常處理。


總結

?????????通過上面的三個實踐,可以加深我們對于AOP的理解和應用。通過Spring AOP對我們的服務進行抽象處理,簡化我們的開發和維護成本,寫出更加高質量的代碼。


github鏈接:https://github.com/Djhhhhhh/aop-demo

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

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

相關文章

Rust 語言語法糖深度解析:優雅背后的編譯器魔法

之前介紹了語法糖的基本概念和在C/Python/JavaScript中的使用&#xff0c;今天和大家討論語法糖在Rust中的表現形式。 程序語言中的語法糖&#xff1a;讓代碼更優雅的甜味劑 引言&#xff1a;語法糖的本質與價值 語法糖(Syntactic Sugar) 是編程語言中那些并不引入新功能&…

【56】數組指針:指針穿梭數組間

【56】數組指針&#xff1a;指針穿梭數組間 引言 在嵌入式系統開發中&#xff0c;指針操作是優化內存管理和數據交互的核心技術。本文以STC89C52單片機為平臺&#xff0c;通過一維指針強制轉換、二維指針結構化操作和**return返回指針**三種方法&#xff0c;系統講解指針操作二…

C語言【指針二】

引言 介紹&#xff1a;const修飾指針&#xff0c;野指針 應用&#xff1a;指針的使用&#xff08;strlen的模擬實現&#xff09;&#xff0c;傳值調用和傳指調用 一、const修飾指針 1.const修飾變量 簡單回顧一下前面學過的const修飾變量&#xff1a;在變量前面加上const&…

學習記錄-軟件測試基礎

一、軟件測試分類 1.按階段&#xff1a;單元測試&#xff08;一般開發自測&#xff09;、集成測試、系統測試、驗收測試 2.按代碼可見度測試&#xff1a;黑盒測試、灰盒測試、白盒測試 3.其他&#xff1a;冒煙測試(冒煙測試主要是在開發提測后進行&#xff0c;主要是測試主流…

RAG系統實戰:當檢索為空時,如何實現生成模塊的優雅降級(Fallback)?

目錄 RAG系統實戰&#xff1a;當檢索為空時&#xff0c;如何實現生成模塊的優雅降級&#xff08;Fallback&#xff09;&#xff1f; 一、為什么需要優雅降級&#xff08;Fallback&#xff09;&#xff1f; 二、常用的優雅降級策略 策略一&#xff1a;預設后備提示&#xff0…

spring boot前后端開發上傳文件時報413(Request Entity Too Large)錯誤的可能原因及解決方案

可能原因及解決方案 1. Spring Boot默認文件大小限制 原因&#xff1a;Spring Boot默認單文件最大為1MB&#xff0c;總請求體限制為10MB。解決方案&#xff1a; 在application.properties中配置&#xff1a;spring.servlet.multipart.max-file-size10MB # 單文件最大 spring…

Qt - findChild

findChild 1. 函數原型2. 功能描述3. 使用場景4. 示例代碼5. 注意事項6. 總結 在 Qt 中&#xff0c;每個 QObject 都可以擁有子對象&#xff0c;而 QObject 提供的模板函數 findChild 就是用來在對象樹中查找滿足特定條件的子對象的工具。下面我們詳細介紹一下它的使用和注意事…

Sink Token

論文&#xff1a;ICLR 2025 MLLM視覺VAR方法Attention重分配 Sink Token 是一種在語言模型(LLM)和多模態模型(MLLM)中用于優化注意力分配的關鍵機制&#xff0c;通過吸收模型中冗余的注意力權重&#xff0c;確保注意力資源不被無效或無關信息占用。以下是對這一概念的系統性解…

Spring Event 觀察者模型及事件和消息隊列之間的區別筆記

Spring Event觀察者模型&#xff1a;基于內置事件實現自定義監聽 在Spring框架中&#xff0c;觀察者模式通過事件驅動模型實現&#xff0c;允許組件間通過事件發布與監聽進行解耦通信。這一機制的核心在于ApplicationEvent、ApplicationListener和ApplicationEventPublisher等接…

【復活吧,我的愛機!】Ideapad300-15isk拆機升級:加內存條 + 換固態硬盤 + 換電源

寫在前面&#xff1a;本博客僅作記錄學習之用&#xff0c;部分圖片來自網絡&#xff0c;如需引用請注明出處&#xff0c;同時如有侵犯您的權益&#xff0c;請聯系刪除&#xff01; 文章目錄 前言升級成本升級流程電池健康度加內存條和換內存條光驅位加裝機械硬盤更換電池重裝系…

基于PyQt5的自動化任務管理軟件:高效、智能的任務調度與執行管理

基于PyQt5的自動化任務管理軟件&#xff1a;高效、智能的任務調度與執行管理 相關資源文件已經打包成EXE文件&#xff0c;可雙擊直接運行程序&#xff0c;且文章末尾已附上相關源碼&#xff0c;以供大家學習交流&#xff0c;博主主頁還有更多Python相關程序案例&#xff0c;秉著…

JavaScript 庫:全面解析與推薦

JavaScript 庫:全面解析與推薦 引言 JavaScript 作為當今最流行的前端開發語言之一,擁有豐富的庫和框架。這些庫和框架極大地簡化了開發工作,提高了開發效率。本文將全面解析 JavaScript 庫,并推薦一些優秀的庫,幫助開發者更好地掌握 JavaScript。 JavaScript 庫概述 …

C#從入門到精通(5)

目錄 第十二章 其他基礎知識 &#xff08;1&#xff09;抽象類和方法 &#xff08;2&#xff09;接口 &#xff08;3&#xff09;集合與索引器 &#xff08;4&#xff09;委托和匿名方法 &#xff08;5&#xff09;事件 &#xff08;6&#xff09;迭代器 &#xff08;7…

【區塊鏈安全 | 第十四篇】類型之值類型(一)

文章目錄 值類型布爾值整數運算符取模運算指數運算 定點數地址&#xff08;Address&#xff09;類型轉換地址成員balance 和 transfersendcall&#xff0c;delegatecall 和 staticcallcode 和 codehash 合約類型&#xff08;Contract Types&#xff09;固定大小字節數組&#x…

Windows 系統下多功能免費 PDF 編輯工具詳解

IceCream PDF Editor是一款極為實用且操作簡便的PDF文件編輯工具&#xff0c;它完美適配Windows操作系統。其用戶界面設計得十分直觀&#xff0c;哪怕是初次接觸的用戶也能快速上手。更為重要的是&#xff0c;該軟件具備豐富多樣的強大功能&#xff0c;能全方位滿足各類PDF編輯…

vue3相比于vue2的提升

性能提升&#xff1a; Vue3的頁面渲染速度更快、性能更好。特別是在處理大量數據和復雜組件時&#xff0c;優勢更加明顯。Vue3引入了編譯時優化&#xff0c;如靜態節點提升&#xff08;hoistStatic&#xff09;、補丁標志&#xff08;patchflag&#xff09;等&#xff0c;這些…

Redis 梳理匯總目錄

Redis 哨兵集群&#xff08;Sentinel&#xff09;與 Cluster 集群對比-CSDN博客 如何快速將大規模數據保存到Redis集群-CSDN博客 Redis的一些高級指令-CSDN博客 Redis 篇-CSDN博客

【奇點時刻】GPT-4o新生圖特性深度洞察報告

以下報告圍繞最新推出的「GPT4o」最新圖像生成技術展開&#xff0c;旨在讓讀者從整體層面快速了解其技術原理、功能亮點&#xff0c;以及與其他常見圖像生成或AI工具的對比分析&#xff0c;同時也會客觀探討該技術在應用過程中可能遇到的挑戰與限制。 1. 技術背景概述 GPT4o新…

【算法day28】解數獨——編寫一個程序,通過填充空格來解決數獨問題

37. 解數獨 編寫一個程序&#xff0c;通過填充空格來解決數獨問題。 數獨的解法需 遵循如下規則&#xff1a; 數字 1-9 在每一行只能出現一次。 數字 1-9 在每一列只能出現一次。 數字 1-9 在每一個以粗實線分隔的 3x3 宮內只能出現一次。&#xff08;請參考示例圖&#xff…

【已解決】Javascript setMonth跨月問題;2025-03-31 setMonth后變成 2025-05-01

文章目錄 bug重現解決方法&#xff1a;用第三方插件來實現&#xff08;不推薦原生代碼來實現&#xff09;。項目中用的有dayjs。若要自己實現&#xff0c;參考 AI給出方案&#xff1a; bug重現 今天&#xff08;2025-04-01&#xff09;遇到的一個問題。原代碼邏輯大概是這樣的…