【SpringBoot】SpringBoot中使用AOP實現日志記錄功能

在這里插入圖片描述

  • 前言
  • 一、AOP基本概念
  • 二、項目準備
  • 三、實現日志記錄切面
    • 1、創建自定義日志注解
    • 2、實現日志切面
    • 3、配置AOP
  • 四、使用示例
    • 1. 在Controller中使用
    • 2. 在Service中使用
  • 六、高級配置
    • 1. 日志內容格式化
    • 2. 異步日志記錄
    • 3. 日志脫敏處理
  • 七、代理類生成的核心邏輯
    • 問題1: 既然是代理類,那么像@Transaction 注解標注在方法上怎么搞?給方法生成代理類?(顯然不是)
    • 問題2:自定義的注解@LogExecutionTime,也會生成代理類嗎?
  • 八、具體執行步驟

前言

在開發企業級應用時,完善的日志記錄系統對于問題排查、系統監控和用戶行為分析都至關重要。傳統的日志記錄方式往往需要在每個方法中手動添加日志代碼,這不僅增加了代碼量,也使得業務邏輯與日志記錄代碼耦合在一起。Spring
AOP(面向切面編程)為我們提供了一種優雅的解決方案,可以無侵入式地實現日志記錄功能。

本文將詳細介紹如何在SpringBoot項目中利用AOP實現統一的日志記錄功能。

一、AOP基本概念

在開始實現之前,我們先了解幾個AOP的核心概念:

  • 切面(Aspect):橫切關注點的模塊化,如日志記錄就是一個切面

  • 連接點(Joinpoint):程序執行過程中的某個特定點,如方法調用或異常拋出

  • 通知(Advice):在切面的某個連接點上執行的動作

  • 切入點(Pointcut):匹配連接點的謂詞,用于確定哪些連接點需要執行通知

  • 目標對象(Target Object):被一個或多個切面通知的對象

在這里插入圖片描述

二、項目準備

需要創建一個SpringBoot項目,添加以下依賴:

<dependencies><!-- Spring Boot Starter Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Boot Starter AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><!-- Lombok 簡化代碼 --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency>
</dependencies>

三、實現日志記錄切面

1、創建自定義日志注解

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {/*** 操作名稱* @return*/String operation() default "";/*** 操作的類型* @return*/BusinessType businessType() default BusinessType.OTHER;
}

2、實現日志切面

創建一個切面類,實現日志切面功能。

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;@Aspect
@Component
public class LoggingAspect {private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);/*** 定義切入點:所有帶有@Loggable注解的方法*/@Pointcut("@annotation(com.yourpackage.Loggable)")public void loggableMethods() {}/*** 環繞通知:記錄方法執行前后的日志*/@Around("loggableMethods()")public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();Method method = signature.getMethod();Loggable loggable = method.getAnnotation(Loggable.class);String methodName = joinPoint.getTarget().getClass().getName() + "." + method.getName();// 記錄方法開始日志if (loggable.recordParams()) {logger.info("===> 開始執行 {},參數: {}", methodName, Arrays.toString(joinPoint.getArgs()));} else {logger.info("===> 開始執行 {}", methodName);}long startTime = System.currentTimeMillis();try {// 執行目標方法Object result = joinPoint.proceed();// 記錄方法結束日志long elapsedTime = System.currentTimeMillis() - startTime;if (loggable.recordResult()) {logger.info("<=== 執行完成 {},耗時: {}ms,結果: {}", methodName, elapsedTime, result);} else {logger.info("<=== 執行完成 {},耗時: {}ms", methodName, elapsedTime);}return result;} catch (Exception e) {// 記錄異常日志long elapsedTime = System.currentTimeMillis() - startTime;logger.error("<=== 執行異常 {},耗時: {}ms,異常: {}", methodName, elapsedTime, e.getMessage(), e);throw e;}}/*** 對Controller層的方法進行日志記錄*/@Pointcut("execution(* com.yourpackage.controller..*.*(..))")public void controllerLog() {}@Before("controllerLog()")public void doBefore(JoinPoint joinPoint) {// 獲取請求信息ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attributes == null) {return;}HttpServletRequest request = attributes.getRequest();// 記錄請求信息logger.info("==============================請求開始==============================");logger.info("請求URL: {}", request.getRequestURL().toString());logger.info("HTTP方法: {}", request.getMethod());logger.info("IP地址: {}", request.getRemoteAddr());logger.info("類方法: {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());logger.info("請求參數: {}", Arrays.toString(joinPoint.getArgs()));}@AfterReturning(returning = "result", pointcut = "controllerLog()")public void doAfterReturning(Object result) {logger.info("返回結果: {}", result);logger.info("==============================請求結束==============================");}
}

3、配置AOP

確保SpringBoot應用啟用了AOP支持(默認就是啟用的),如果需要自定義配置,可以在application.properties中添加。

# AOP配置
spring.aop.auto=true
spring.aop.proxy-target-class=true

四、使用示例

1. 在Controller中使用

@RestController
@RequestMapping("/api/user")
public class UserController {@GetMapping("/{id}")@Loggable("根據ID獲取用戶信息")public User getUser(@PathVariable Long id) {// 業務邏輯return userService.getUserById(id);}@PostMapping@Loggable(value = "創建新用戶", recordParams = false)public User createUser(@RequestBody User user) {// 業務邏輯return userService.createUser(user);}
}

2. 在Service中使用

@Service
public class UserService {@Loggable("根據ID查詢用戶")public User getUserById(Long id) {// 業務邏輯}@Loggable(value = "創建用戶", recordResult = false)public User createUser(User user) {// 業務邏輯}
}

六、高級配置

1. 日志內容格式化

我們可以創建一個工具類來美化日志輸出:

public class LogFormatUtils {public static String formatMethodCall(String className, String methodName, Object[] args) {StringBuilder sb = new StringBuilder();sb.append(className).append(".").append(methodName).append("(");if (args != null && args.length > 0) {for (int i = 0; i < args.length; i++) {if (i > 0) {sb.append(", ");}sb.append(formatArg(args[i]));}}sb.append(")");return sb.toString();}private static String formatArg(Object arg) {if (arg == null) {return "null";}// 對于基本類型和字符串直接返回if (arg instanceof Number || arg instanceof Boolean || arg instanceof Character || arg instanceof String) {return arg.toString();}// 對于集合和數組,只顯示大小if (arg.getClass().isArray()) {return "array[" + Array.getLength(arg) + "]";}if (arg instanceof Collection) {return "collection[" + ((Collection<?>) arg).size() + "]";}if (arg instanceof Map) {return "map[" + ((Map<?, ?>) arg).size() + "]";}// 其他復雜對象只顯示類名return arg.getClass().getSimpleName();}
}

然后在切面中使用:

@Around("loggableMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {MethodSignature signature = (MethodSignature) joinPoint.getSignature();String methodCall = LogFormatUtils.formatMethodCall(joinPoint.getTarget().getClass().getSimpleName(),signature.getName(),joinPoint.getArgs());logger.info("===> 調用: {}", methodCall);// ... 其他邏輯
}

2. 異步日志記錄

對于一些并發場景,可以考慮異步記錄日志以減少性能影響:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {@Overridepublic Executor getAsyncExecutor() {ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();executor.setCorePoolSize(5);executor.setMaxPoolSize(10);executor.setQueueCapacity(100);executor.setThreadNamePrefix("AsyncLogger-");executor.initialize();return executor;}
}// 然后在切面方法上添加@Async注解
@Async
@Around("loggableMethods()")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {// 日志記錄邏輯
}

3. 日志脫敏處理

對于敏感信息如手機號、身份證號等,需要進行脫敏處理:

public class SensitiveInfoUtils {private static final String PHONE_REGEX = "(\\d{3})\\d{4}(\\d{4})";private static final String ID_CARD_REGEX = "(\\d{4})\\d{10}(\\w{4})";public static String desensitize(Object arg) {if (arg == null) {return null;}String str = arg.toString();// 手機號脫敏if (str.matches("\\d{11}")) {return str.replaceAll(PHONE_REGEX, "$1****$2");}// 身份證號脫敏if (str.matches("\\d{18}|\\d{17}[xX]")) {return str.replaceAll(ID_CARD_REGEX, "$1**********$2");}// 其他敏感信息處理...return str;}
}// 在LogFormatUtils中使用
private static String formatArg(Object arg) {// ... 其他邏輯return SensitiveInfoUtils.desensitize(arg);
}

七、代理類生成的核心邏輯

問題1: 既然是代理類,那么像@Transaction 注解標注在方法上怎么搞?給方法生成代理類?(顯然不是)

@Transactional 雖然通常標注在方法上,但 Spring 的代理生成策略會綜合考慮 類級別和方法級別 的注解。
以下是其完整工作原理:
Spring 處理 @Transactional 時,代理生成分為兩步:

步驟 1:掃描 Bean 的代理需求

  • 類級別檢查:Spring 在創建 Bean 時,會檢查 類或父類 是否有 @Transactional 注解(類級別注解會影響所有方法)。

  • 方法級別檢查:如果類未被注解,則掃描所有 公有方法(public),發現任意方法有 @Transactional 時,整個類會被代理

步驟 2:生成代理對象

  • JDK 動態代理:如果類實現了接口,默認使用 JDK 代理(基于 InvocationHandler)。

  • CGLIB 代理:如果類未實現接口,則生成子類代理(通過字節碼增強)。

問題2:自定義的注解@LogExecutionTime,也會生成代理類嗎?

自定義的注解 @LogExecutionTime 是否會生成代理類,取決于如何實現這個注解的功能。如果通過 Spring AOP 來實現 @LogExecutionTime 的功能(例如記錄方法執行時間),那么確實會生成代理類。比如下面的代碼

//自定義一個注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Retention(RetentionPolicy.RUNTIME) // 注解在運行時保留
@Target(ElementType.METHOD) // 注解可以應用于方法
public @interface LogExecutionTime {
}//定義切面類,使用 @Aspect 注解標記,并在其中定義切點(Pointcut)和通知(Advice)。
//切點通過 @annotation(LogExecutionTime) 匹配所有帶有 @LogExecutionTime 注解的方法。
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;@Aspect // 聲明這是一個切面類
@Component // 確保切面類被 Spring 容器管理
public class LogExecutionTimeAspect {// 定義環繞通知,匹配所有帶有 @LogExecutionTime 注解的方法@Around("@annotation(LogExecutionTime)")public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {long startTime = System.currentTimeMillis(); // 記錄方法開始執行時間// 執行目標方法Object result = joinPoint.proceed();long executionTime = System.currentTimeMillis() - startTime; // 計算方法執行時間// 輸出日志System.out.println("Method " + joinPoint.getSignature().getName() + " executed in " + executionTime + "ms");return result; // 返回目標方法的執行結果}
}//在業務方法上面使用這個自定義注解
@Service
public class MyService {@LogExecutionTime // 標記需要記錄執行時間的方法public void myBusinessMethod() {// 模擬業務邏輯try {Thread.sleep(1000); // 模擬耗時操作} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Business logic executed.");}
}

那么生成代理類的流程是這樣的,

  1. 切面類注冊:Spring 掃描到 @Aspect 標注的類(如 LogExecutionTimeAspect ngAspect),將其注冊為 Bean,并識別其中的切點和通知。(切面類不生成代理類)
  2. 接著根據切點,再掃描符合條件的bean,**@annotation(LogExecutionTime)**所有帶有 @LogExecutionTime 注解的方法所在的類對應的bean都會生成代理類。

八、具體執行步驟

給符合條件的bean,生成代理類之后,就要開始執行了,由于不確定是哪一個方法加了@Transactional這種所謂的aop注解,所以代理類,會對每一個方法進行檢查。具體步驟如下:

步驟 1:調用代理對象的方法

// 這里實際調用的是代理對象的方法,代理對象會去調用目標對象的原方法
userService.getUserById(1L);

步驟 2:代理對象檢查方法是否匹配切點

檢查目標方法:代理對象會檢查 UserService.getUserById() 是否匹配切面定義的切點(即是否有 @LogExecutionTime 注解)。

// 偽代碼:Spring 的切點匹配邏輯
if (方法有 @LogExecutionTime 注解 || 類有 @LogExecutionTime 注解) {將該方法加入攔截鏈;
}

步驟 3:執行攔截鏈

如果方法匹配切點,代理對象會:

找到所有匹配的 Advice(如 @Around 通知)。

按順序執行通知鏈(如先執行 @Before,再執行 @Around)。

在 @Around 中,通過 ProceedingJoinPoint.proceed() 調用原始方法。

在這里插入圖片描述

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

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

相關文章

linux中的常用命令(一)

目錄 常用的快捷鍵 1- tab鍵:命令或者路徑提示及補全&#xff1b; 2-ctrlc:放棄當前輸入&#xff0c;終止當前任務或程序 3-ctrll;清屏 4-ctrlinsert:復制 5-鼠標右鍵:粘貼&#xff1b; 6-altc:斷開連接/ctrlshift r 重新連接 7-alt1/2/3/等&#xff1a;切換回話窗口 8-上下鍵…

Pycharm(十九)深度學習

一、深度學習概述 1.1 什么是深度學習 深度學習是機器學習中的一種特殊方法,它使用稱為神經網絡的復雜結構,特別是“深層”的神經網絡,來學習和做出預測。深度學習特別適合處理大規模和高維度的數據,如圖像、聲音和文本。深度學習、機器學習和人工智能之間的關系如下圖所…

多視圖密集對應學習:細粒度3D分割的自監督革命

原文標題&#xff1a;Multi-view Dense Correspondence Learning (MvDeCor) 引言 在計算機視覺與圖形學領域&#xff0c;3D形狀分割一直是一個基礎且具有挑戰性的任務。如何在標注稀缺的情況下&#xff0c;實現對3D模型的細粒度分割&#xff1f;近期&#xff0c;斯坦福大學視覺…

Vue——前端vue3項目使用漢字轉拼音

在 Vue3 項目中&#xff0c;可以通過以下 第三方 JavaScript 包 實現漢字轉拼音。這些包均兼容 Vue3&#xff0c;且無需依賴后端處理&#xff1a; 推薦方案 1. pinyin-pro 特點&#xff1a;功能強大、支持多音字、聲調、拼音匹配、輕量級&#xff08;~20KB&#xff09;。安裝…

批量統計PDF頁數,統計圖像屬性

軟件介紹&#xff1a; 1、支持批量統計PDF、doc\docx、xls\xlsx頁數 2、支持統計指定格式文件數量&#xff08;不填格式就是全部&#xff09; 3、支持統計JPG、JPEG、PNG圖像屬性 4、支持統計多頁TIF頁數、屬性 5、支持統計PDF、JPG畫幅 統計圖像屬性 「托馬斯的文件助手」…

LeetCode 每日一題 2025/5/5-2025/5/11

記錄了初步解題思路 以及本地實現代碼&#xff1b;并不一定為最優 也希望大家能一起探討 一起進步 目錄 5/5 790. 多米諾和托米諾平鋪5/6 1920. 基于排列構建數組5/7 3341. 到達最后一個房間的最少時間 I5/8 3342. 到達最后一個房間的最少時間 II5/9 3343. 統計平衡排列的數目5…

pytest自動化測試執行環境切換的兩種解決方案

&#x1f345; 點擊文末小卡片&#xff0c;免費獲取軟件測試全套資料&#xff0c;資料在手&#xff0c;漲薪更快 一、痛點分析 在實際企業的項目中&#xff0c;自動化測試的代碼往往需要在不同的環境中進行切換&#xff0c;比如多套測試環境、預上線環境、UAT環境、線上環…

visual studio 2015 安裝閃退問題

參考鏈接&#xff1a; VS2012安裝時啟動界面一閃而過問題解決辦法 visual studio 2015 安裝閃退問題

RocketMQ Kafka區別

架構 ZooKeeper&#xff1a;管理 Broker 注冊、分區 Leader 選舉及消費者組狀態。Broker&#xff1a;存儲 Partition數據&#xff0c;每個 Partition 為獨立日志文件。Producer/Consumer&#xff1a;通過 ZooKeeper獲取路由信息&#xff0c;實現消息分發與消費。 NameServer&am…

MySQL進階篇2_SQL優化、鎖

文章目錄 1 SQL優化1.1插入數據優化1.2主鍵優化頁分裂頁合并主鍵設計原則 1.3order by設計優化1.4group by設計優化小理解 1.5limit設計優化順序IO和隨機IO小疑惑 1.6count設計優化1.7update優化關于隱式事務事務的DML操作 鎖全局鎖表級鎖表鎖元數據鎖意向鎖 行級鎖鎖的釋放條件…

如何測試 esp-webrtc-solution_solutions_doorbell_demo 例程?

軟件準備 esp-webrtc-solution/solutions/doorbell_demo 例程 此例程集成了 WebSocket 傳輸視頻流的應用 硬件準備 ESP32P4-Function-Ev-Board 環境搭建 推薦基于 esp-idf v5.4.1 版本的環境來編譯此例程 若編譯時出現依賴的組件報錯&#xff0c;可進行如下修改&#xff…

TransmittableThreadLocal:穿透線程邊界的上下文傳遞藝術

文章目錄 前言一、如何線程上下文傳遞1.1 ThreadLocal單線程1.2 InheritableThreadLocal的繼承困境1.3 TTL的時空折疊術 二、TTL核心設計解析2.1 時空快照機制2.2 裝飾器模式2.3 采用自動清理機制 三、設計思想啟示四、實踐啟示錄結語 前言 在并發編程領域&#xff0c;線程上下…

【數據結構】——棧

一、棧的概念和結構 棧其實就是一種特殊的順序表&#xff0c;其只允許在一端進出&#xff0c;就是棧的數據的插入和刪除只能在一端進行&#xff0c;進行數據的插入和刪除操作的一端稱為棧頂&#xff0c;另一端稱為棧底。棧中的元素遵循先進后出LIFO&#xff08;Last InFirst O…

大數據技術全景解析:Spark、Hadoop、Hive與SQL的協作與實戰

引言&#xff1a;當數據成為新時代的“石油” 在數字經濟時代&#xff0c;數據量以每年50%的速度爆發式增長。如何高效存儲、處理和分析PB級數據&#xff0c;成為企業競爭力的核心命題。本文將通過通俗類比場景化拆解&#xff0c;帶你深入理解四大關鍵技術&#xff1a;Hadoop、…

Android13 權限管理機制整理

一、概述 權限機制作為Android 系統安全的保證,很重要,這里整理一下 權限機制中framework 部分,selinux等其他的Android權限機制不在本次討論范圍內 二、個版本差異分類 Android13 Android12 Android11 及以下 拋開版本差異權限機制分為兩大類 一類是之前apk在Android6.0…

MySQL的Order by與Group by優化詳解!

目錄 前言核心思想&#xff1a;讓索引幫你“排好序”或“分好組”Part 1: ORDER BY 優化詳解1.1 什么是 Filesort&#xff1f;為什么它慢&#xff1f;1.2 如何避免 Filesort&#xff1f;—— 利用索引的有序性1.3 EXPLAIN 示例 (ORDER BY) Part 2: GROUP BY 優化詳解2.1 什么是…

awesome-digital-human本地部署及配置:打造高情緒價值互動指南

在數字化交互的浪潮中&#xff0c;awesome-digital-human-live2d項目為我們打開了本地數字人互動的大門。結合 dify 聊天 api&#xff0c;并借鑒 coze 夸夸機器人的設計思路&#xff0c;能為用戶帶來充滿情緒價值的交互體驗。本文將詳細介紹其本地部署步驟、dify 配置方法及情緒…

[ctfshow web入門] web68

信息收集 highlight_file被禁用了&#xff0c;使用cinclude("php://filter/convert.base64-encode/resourceindex.php");讀取index.php&#xff0c;使用cinclude("php://filter/convert.iconv.utf8.utf16/resourceindex.php");可能有些亂碼&#xff0c;不…

計算機網絡:深度解析基于鏈路狀態的內部網關協議IS-IS

IS-IS(Intermediate System to Intermediate System)路由協議詳解 IS-IS(Intermediate System to Intermediate System)是一種基于鏈路狀態的內部網關協議(IGP),最初由ISO為OSI(開放系統互連)模型設計,后經擴展支持IP路由。它廣泛應用于大型運營商網絡、數據中心及復…

SEGGER項目

SystemView 查看版本, 查看SEGGER官網&#xff0c;release時間是2019-12-18日, 而3.12.0的版本日期是2020-05-04 #define SEGGER_SYSVIEW_MAJOR 3 #define SEGGER_SYSVIEW_MINOR 10 #define SEGGER_SYSVIEW_REV 0SEGGER EMBEDDED Studio 根據S…