基于SpringBoot+AOP實(shí)現(xiàn)操作日志記錄
今天就來講講Spring AOP最實(shí)用的實(shí)戰(zhàn)場(chǎng)景——用 SpringBoot + AOP 實(shí)現(xiàn)操作日志記錄。
操作日志是項(xiàng)目必備功能:比如用戶登錄、接口調(diào)用、數(shù)據(jù)新增/修改/刪除,都需要記錄操作人、操作時(shí)間、操作內(nèi)容、接口地址等信息,方便后續(xù)排查問題、審計(jì)追溯。
一、明確操作日志要記錄哪些信息?
先梳理操作日志的核心字段,避免后續(xù)代碼遺漏,實(shí)戰(zhàn)中可根據(jù)項(xiàng)目需求增減,這里給出通用模板:
- 操作人:當(dāng)前登錄用戶的用戶名/ID(實(shí)戰(zhàn)中結(jié)合 Spring Security 或 Token 獲?。?;
- 操作時(shí)間:接口執(zhí)行的時(shí)間(精確到毫秒);
- 操作模塊:比如“用戶管理”“訂單管理”“商品管理”(標(biāo)記當(dāng)前操作屬于哪個(gè)模塊);
- 操作描述:比如“新增用戶”“刪除訂單”“查詢商品列表”(清晰說明操作內(nèi)容);
- 接口地址:被調(diào)用的接口 URL(比如 /api/user/add);
- 請(qǐng)求方式:GET/POST/PUT/DELETE;
- 請(qǐng)求參數(shù):接口接收的參數(shù)(JSON 格式);
- 返回結(jié)果:接口返回的數(shù)據(jù)(JSON 格式);
- 執(zhí)行狀態(tài):成功/失敗;
- 異常信息:如果接口執(zhí)行失敗,記錄異常詳情(便于排查);
- 操作 IP:調(diào)用接口的客戶端 IP 地址。
二、從零實(shí)現(xiàn) AOP 操作日志
步驟1:搭建基礎(chǔ)環(huán)境(導(dǎo)入依賴)
SpringBoot 項(xiàng)目中,實(shí)現(xiàn) AOP 只需導(dǎo)入 spring-boot-starter-aop 依賴,無需額外導(dǎo)入其他包,在 pom.xml 中添加:
<!-- Spring AOP 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 工具包:用于 JSON 格式化、IP 地址獲?。蛇x,簡(jiǎn)化代碼) -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.32</version>
</dependency>
<!-- 用于獲取客戶端 IP(可選,也可自己寫工具類) -->
<dependency>
<groupId>eu.bitwalker</groupId>
<artifactId>UserAgentUtils</artifactId>
<version>1.21</version>
</dependency>說明:fastjson2 用于將請(qǐng)求參數(shù)、返回結(jié)果轉(zhuǎn)為 JSON 字符串;UserAgentUtils 用于獲取客戶端 IP 和瀏覽器信息,可根據(jù)需求選擇是否導(dǎo)入。
步驟2:創(chuàng)建操作日志實(shí)體類(存儲(chǔ)日志數(shù)據(jù))
創(chuàng)建實(shí)體類 OperationLog,對(duì)應(yīng)操作日志的核心字段,后續(xù)可直接映射到數(shù)據(jù)庫(kù)(這里省略數(shù)據(jù)庫(kù)操作,重點(diǎn)放在 AOP 實(shí)現(xiàn)):
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作日志實(shí)體類
*/
@Data
public class OperationLog {
// 主鍵(實(shí)戰(zhàn)中可自增)
private Long id;
// 操作人(用戶名/ID)
private String operator;
// 操作時(shí)間
private LocalDateTime operationTime;
// 操作模塊
private String module;
// 操作描述
private String description;
// 接口地址
private String requestUrl;
// 請(qǐng)求方式
private String requestMethod;
// 請(qǐng)求參數(shù)(JSON 格式)
private String requestParams;
// 返回結(jié)果(JSON 格式)
private String responseResult;
// 執(zhí)行狀態(tài)(0-失敗,1-成功)
private Integer status;
// 異常信息(失敗時(shí)填寫)
private String errorMsg;
// 操作 IP
private String operationIp;
}說明:用 @Data 注解(lombok)簡(jiǎn)化 getter/setter 方法,實(shí)戰(zhàn)中需導(dǎo)入 lombok 依賴(如果未導(dǎo)入)。
步驟3:創(chuàng)建自定義注解(精準(zhǔn)定位需要記錄日志的接口)
我們用「自定義注解」來標(biāo)記需要記錄操作日志的接口,這樣可以靈活控制哪些接口需要記錄日志,哪些不需要——比直接用切點(diǎn)表達(dá)式匹配包/類更靈活。
import java.lang.annotation.*;
/**
* 自定義操作日志注解
* @Target:注解作用范圍(METHOD:作用在方法上)
* @Retention:注解保留時(shí)機(jī)(RUNTIME:運(yùn)行時(shí)保留,AOP 可獲取)
* @Documented:生成文檔時(shí)包含該注解
*/
@Target(ElementType.METHOD) // 只作用于方法
@Retention(RetentionPolicy.RUNTIME) // 運(yùn)行時(shí)生效
@Documented
public @interface OperationLogAnnotation {
// 操作模塊(必填,比如“用戶管理”)
String module() default "";
// 操作描述(必填,比如“新增用戶”)
String description() default "";
}說明:該注解有兩個(gè)屬性,module(操作模塊)和 description(操作描述),在需要記錄日志的接口方法上添加該注解,并填寫對(duì)應(yīng)屬性即可。
步驟4:創(chuàng)建 AOP 切面(實(shí)現(xiàn)日志記錄)
這是本次實(shí)戰(zhàn)的核心,創(chuàng)建切面類,定義切點(diǎn)(匹配帶有 @OperationLogAnnotation 注解的方法)、通知(環(huán)繞通知,實(shí)現(xiàn)日志記錄邏輯),完成日志的收集和處理。
import com.alibaba.fastjson2.JSON;
import eu.bitwalker.useragentutils.UserAgent;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
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.time.LocalDateTime;
import java.util.Arrays;
/**
* 操作日志切面類
* @Aspect:標(biāo)記此類為切面
* @Component:交給 Spring 管理,讓 Spring 掃描到該切面
*/
@Aspect
@Component
public class OperationLogAspect {
// 1. 定義切點(diǎn):匹配帶有 @OperationLogAnnotation 注解的方法
@Pointcut("@annotation(com.example.demo.annotation.OperationLogAnnotation)")
public void operationLogPointcut() {} // 切點(diǎn)方法,無實(shí)際邏輯,僅用于標(biāo)記
// 2. 定義環(huán)繞通知:包裹切點(diǎn)方法,可在方法執(zhí)行前、執(zhí)行后、異常時(shí)處理
@Around("operationLogPointcut()")
public Object recordOperationLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 初始化操作日志對(duì)象
OperationLog operationLog = new OperationLog();
// 2. 獲取當(dāng)前請(qǐng)求對(duì)象,用于獲取請(qǐng)求信息(URL、請(qǐng)求方式、IP 等)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 3. 填充日志基礎(chǔ)信息(無論接口成功/失敗,都需要記錄)
// 3.1 獲取操作人(實(shí)戰(zhàn)中需結(jié)合 Spring Security/Token 獲取,這里模擬 admin)
operationLog.setOperator("admin");
// 3.2 操作時(shí)間(當(dāng)前時(shí)間)
operationLog.setOperationTime(LocalDateTime.now());
// 3.3 接口地址
operationLog.setRequestUrl(request.getRequestURI());
// 3.4 請(qǐng)求方式(GET/POST)
operationLog.setRequestMethod(request.getMethod());
// 3.5 操作 IP(獲取客戶端真實(shí) IP)
operationLog.setOperationIp(getClientIp(request));
// 3.6 請(qǐng)求參數(shù)(將方法參數(shù)轉(zhuǎn)為 JSON 字符串)
Object[] args = joinPoint.getArgs();
operationLog.setRequestParams(JSON.toJSONString(args));
// 4. 獲取自定義注解的屬性(模塊、描述)
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperationLogAnnotation annotation = method.getAnnotation(OperationLogAnnotation.class);
operationLog.setModule(annotation.module());
operationLog.setDescription(annotation.description());
// 5. 執(zhí)行目標(biāo)方法(核心業(yè)務(wù)邏輯),捕獲執(zhí)行結(jié)果和異常
Object result = null;
try {
// 執(zhí)行目標(biāo)方法(比如接口的核心邏輯)
result = joinPoint.proceed();
// 方法執(zhí)行成功:設(shè)置狀態(tài)為 1(成功),記錄返回結(jié)果
operationLog.setStatus(1);
operationLog.setResponseResult(JSON.toJSONString(result));
} catch (Throwable throwable) {
// 方法執(zhí)行失敗:設(shè)置狀態(tài)為 0(失?。涗洰惓P畔?
operationLog.setStatus(0);
operationLog.setErrorMsg(throwable.getMessage());
// 拋出異常,不影響原有業(yè)務(wù)邏輯的異常處理
throw throwable;
} finally {
// 6. 日志持久化(實(shí)戰(zhàn)中可存入數(shù)據(jù)庫(kù)、ElasticSearch 等,這里模擬打?。?
System.out.println("操作日志記錄:" + JSON.toJSONString(operationLog, true));
// TODO: 實(shí)戰(zhàn)中替換為數(shù)據(jù)庫(kù)插入操作(比如調(diào)用 OperationLogService.save(operationLog))
}
// 返回目標(biāo)方法的執(zhí)行結(jié)果,不影響原有接口的返回值
return result;
}
/**
* 工具方法:獲取客戶端真實(shí) IP(處理代理場(chǎng)景,比如 Nginx 代理)
*/
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 處理多代理場(chǎng)景,取第一個(gè)非 unknown 的 IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}核心解讀:
- 切點(diǎn):通過 @annotation 匹配帶有自定義注解的方法,精準(zhǔn)控制需要記錄日志的接口;
- 環(huán)繞通知:用 @Around 包裹目標(biāo)方法,先收集請(qǐng)求信息、注解屬性,再執(zhí)行目標(biāo)方法,最后處理日志(成功/失敗);
- IP 獲取:處理了 Nginx 代理等場(chǎng)景,確保獲取到客戶端真實(shí) IP;
- 異常處理:捕獲目標(biāo)方法的異常,記錄異常信息,同時(shí)重新拋出異常,不影響原有業(yè)務(wù)的異常處理邏輯;
- 日志持久化:這里用打印模擬,實(shí)戰(zhàn)中需替換為數(shù)據(jù)庫(kù)插入、ES 存儲(chǔ)等邏輯。
步驟5:接口測(cè)試(驗(yàn)證日志記錄效果)
創(chuàng)建一個(gè)測(cè)試接口,添加自定義 @OperationLogAnnotation 注解,啟動(dòng)項(xiàng)目,調(diào)用接口,查看日志是否正常記錄。
import com.example.demo.annotation.OperationLogAnnotation;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 測(cè)試接口:用戶管理模塊
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
// 添加 @OperationLogAnnotation 注解,標(biāo)記需要記錄日志
@OperationLogAnnotation(module = "用戶管理", description = "新增用戶")
@PostMapping("/add")
public Map<String, Object> addUser(@RequestBody Map<String, String> params) {
// 模擬新增用戶核心邏輯
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "新增用戶成功");
result.put("data", params);
return result;
}
// 測(cè)試異常場(chǎng)景
@OperationLogAnnotation(module = "用戶管理", description = "刪除用戶")
@DeleteMapping("/delete/{id}")
public Map<String, Object> deleteUser(@PathVariable Long id) {
// 模擬異常(比如刪除不存在的用戶)
if (id <= 0) {
throw new RuntimeException("用戶ID非法,無法刪除");
}
Map<String, Object> result = new HashMap<>();
result.put("code", 200);
result.put("msg", "刪除用戶成功");
return result;
}
}測(cè)試1:調(diào)用新增用戶接口
請(qǐng)求地址:http://localhost:8080/api/user/add
請(qǐng)求方式:POST
請(qǐng)求參數(shù):{"username":"test","password":"123456"}
控制臺(tái)打印的日志(格式化后):
操作日志記錄:{
"description":"新增用戶",
"module":"用戶管理",
"operationIp":"127.0.0.1",
"operationTime":"2026-04-14T15:30:00",
"operator":"admin",
"requestMethod":"POST",
"requestParams":"[{\"password\":\"123456\",\"username\":\"test\"}]",
"requestUrl":"/api/user/add",
"responseResult":"{\"code\":200,\"data\":{\"password\":\"123456\",\"username\":\"test\"},\"msg\":\"新增用戶成功\"}",
"status":1
}測(cè)試2:調(diào)用刪除用戶接口
請(qǐng)求地址:http://localhost:8080/api/user/delete/-1
請(qǐng)求方式:DELETE
控制臺(tái)打印的日志(格式化后):
操作日志記錄:{
"description":"刪除用戶",
"errorMsg":"用戶ID非法,無法刪除",
"module":"用戶管理",
"operationIp":"127.0.0.1",
"operationTime":"2026-04-14T15:35:00",
"operator":"admin",
"requestMethod":"DELETE",
"requestParams":"[-1]",
"requestUrl":"/api/user/delete/-1",
"responseResult":"null",
"status":0
}驗(yàn)證結(jié)果:兩種場(chǎng)景的日志都正常記錄,包含了所有核心字段,符合預(yù)期!
三、優(yōu)化技巧
上面的基礎(chǔ)實(shí)現(xiàn)已經(jīng)能滿足大部分項(xiàng)目需求,下面補(bǔ)充3個(gè)實(shí)戰(zhàn)常用的優(yōu)化點(diǎn),讓日志功能更完善。
優(yōu)化1:獲取真實(shí)操作人(替換模擬值)
實(shí)戰(zhàn)中,操作人不能用模擬的“admin”,需結(jié)合 Spring Security 或 Token 解析獲取當(dāng)前登錄用戶:
// 結(jié)合 Spring Security 獲取當(dāng)前登錄用戶
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !(authentication.getPrincipal() instanceof String)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
operationLog.setOperator(userDetails.getUsername()); // 獲取用戶名
}優(yōu)化2:日志持久化(存入數(shù)據(jù)庫(kù))
創(chuàng)建 OperationLogService 和 OperationLogMapper,將日志對(duì)象存入數(shù)據(jù)庫(kù)(以 MyBatis-Plus 為例):
// 1. 注入 OperationLogService
@Autowired
private OperationLogService operationLogService;
// 2. 在 finally 中替換打印邏輯,改為存入數(shù)據(jù)庫(kù)
finally {
operationLogService.save(operationLog); // MyBatis-Plus 自帶的保存方法
}優(yōu)化3:忽略敏感參數(shù)(避免日志泄露)
接口參數(shù)中可能包含密碼、手機(jī)號(hào)等敏感信息,需要忽略這些參數(shù),避免日志泄露,可自定義注解+攔截處理:
// 1. 自定義忽略敏感參數(shù)注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreSensitive {
}
// 2. 在實(shí)體類敏感字段上添加注解
@Data
public class User {
private Long id;
private String username;
@IgnoreSensitive // 忽略密碼字段
private String password;
}
// 3. 在切面中,處理敏感參數(shù)(替換為 ****)
// (核心邏輯:反射獲取字段,判斷是否有 @IgnoreSensitive 注解,有則替換值)四、注意事項(xiàng)
切面類忘記加 @Component 注解
? 錯(cuò)誤做法:只加 @Aspect 標(biāo)記切面,忘記加 @Component;
? 正確做法:@Aspect 只是標(biāo)記切面,必須加 @Component 交給 Spring 管理,否則 Spring 無法掃描到切面,日志記錄失效。
環(huán)繞通知中忘記調(diào)用 joinPoint.proceed()
? 錯(cuò)誤做法:只收集日志,不執(zhí)行目標(biāo)方法,導(dǎo)致接口無法正常返回;
? 正確做法:必須調(diào)用 joinPoint.proceed() 執(zhí)行目標(biāo)方法,同時(shí)接收返回結(jié)果,否則核心業(yè)務(wù)邏輯無法執(zhí)行。
請(qǐng)求參數(shù)為 MultipartFile(文件上傳)時(shí),JSON 格式化報(bào)錯(cuò)
? 錯(cuò)誤表現(xiàn):文件上傳接口,日志記錄時(shí),JSON.toJSONString(args) 報(bào)錯(cuò);
? 解決方案:判斷參數(shù)類型,如果是 MultipartFile,不進(jìn)行 JSON 格式化,直接標(biāo)記為“文件上傳”。
文末小結(jié)
用 SpringBoot + AOP 實(shí)現(xiàn)操作日志,核心就是“自定義注解標(biāo)記接口 + 切面收集日志信息 + 環(huán)繞通知處理增強(qiáng)”,全程無侵入式編碼,復(fù)用性極高。
記?。篈OP 的核心是“解耦”,把日志這種通用功能,和核心業(yè)務(wù)邏輯分離,既保證了核心代碼的簡(jiǎn)潔,又方便后續(xù)維護(hù)和擴(kuò)展。
以上就是基于SpringBoot+AOP實(shí)現(xiàn)操作日志記錄的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot AOP日志記錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot Logback日志記錄到數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方法
這篇文章主要介紹了SpringBoot Logback日志記錄到數(shù)據(jù)庫(kù)的實(shí)現(xiàn)方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-11-11
Java多線程讀寫鎖ReentrantReadWriteLock類詳解
本文詳細(xì)講解了Java多線程讀寫鎖ReentrantReadWriteLock類,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12
SpringBoot結(jié)合WebSocket實(shí)現(xiàn)聊天功能
本文介紹了如何使用SpringBoot和WebSocket實(shí)現(xiàn)一個(gè)簡(jiǎn)單的聊天功能,包括導(dǎo)入依賴、配置類、創(chuàng)建消息實(shí)體、指定ServerEndpoint、創(chuàng)建客戶端等步驟,通過具體示例,演示了如何發(fā)送個(gè)人消息和群發(fā)消息,實(shí)現(xiàn)了基本的聊天功能,適合需要在項(xiàng)目中實(shí)現(xiàn)實(shí)時(shí)通訊功能的開發(fā)者參考2024-11-11
Java toString方法重寫工具之ToStringBuilder案例詳解
這篇文章主要介紹了Java toString方法重寫工具之ToStringBuilder案例詳解,本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-08-08
Java并發(fā)編程之ReentrantLock可重入鎖的實(shí)例代碼
這篇文章主要介紹了Java并發(fā)編程之ReentrantLock可重入鎖的實(shí)例代碼,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02

