SpringBoot使用AOP優(yōu)雅實(shí)現(xiàn)系統(tǒng)操作日志的持久化
在日常開發(fā)中,操作日志是系統(tǒng)不可或缺的一部分——它用于追溯用戶行為、排查問題、審計(jì)安全操作。但如果在每個(gè)業(yè)務(wù)方法中硬編碼日志邏輯,會導(dǎo)致代碼耦合度高、重復(fù)工作量大、維護(hù)困難。本文將基于 AOP(面向切面編程) 思想,結(jié)合Spring生態(tài),實(shí)現(xiàn)一套“業(yè)務(wù)與日志解耦、可復(fù)用、易擴(kuò)展”的操作日志方案,附完整代碼和關(guān)鍵問題解決方案。
一、方案背景與核心設(shè)計(jì)
1.1 為什么選擇AOP
傳統(tǒng)日志實(shí)現(xiàn)的痛點(diǎn):
- 日志邏輯與業(yè)務(wù)邏輯混雜(如每個(gè)Service方法都寫“記錄日志”代碼);
- 新增日志需求時(shí),需修改所有相關(guān)業(yè)務(wù)代碼;
- 日志格式/內(nèi)容調(diào)整時(shí),全局修改成本高。
AOP的優(yōu)勢恰好解決這些問題:
- 解耦:日志邏輯作為“切面”獨(dú)立存在,不侵入業(yè)務(wù)代碼;
- 復(fù)用:通過“切點(diǎn)”批量攔截目標(biāo)方法,統(tǒng)一日志邏輯;
- 靈活:新增/修改日志規(guī)則時(shí),只需調(diào)整切面,無需改動業(yè)務(wù)。
1.2 核心架構(gòu)設(shè)計(jì)
本方案采用“注解標(biāo)記+AOP攔截+接口解耦+數(shù)據(jù)庫存儲”的架構(gòu),避免模塊間循環(huán)依賴,同時(shí)保證擴(kuò)展性。架構(gòu)圖如下:

各組件職責(zé):
- @OperationLog注解:標(biāo)記需要記錄日志的業(yè)務(wù)方法,指定“操作模塊”“操作描述”;
- AOP切面(OperationLogAspect):攔截注解標(biāo)記的方法,收集請求IP、方法信息、參數(shù)、耗時(shí)等;
- LogHandler接口:定義日志保存規(guī)范,解耦Common模塊與業(yè)務(wù)模塊(避免循環(huán)依賴);
- SysOperationLog實(shí)體:封裝日志數(shù)據(jù),映射數(shù)據(jù)庫表;
- 數(shù)據(jù)庫表:持久化存儲日志數(shù)據(jù)。
二、環(huán)境準(zhǔn)備
需引入的核心依賴(Spring Boot項(xiàng)目為例):
<!-- Spring AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- MyBatis(操作數(shù)據(jù)庫) -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<!-- Lombok(簡化實(shí)體類代碼) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- MySQL驅(qū)動 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Java 8時(shí)間類型支持(LocalDateTime映射MySQL datetime) -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-typehandlers-jsr310</artifactId>
<version>1.0.1</version>
</dependency>
三、分步實(shí)現(xiàn)詳解
3.1 步驟1:自定義操作日志注解(@OperationLog)
通過注解標(biāo)記需要記錄日志的方法,并攜帶“操作模塊”“操作描述”等元信息(放在Common公共模塊)。
import java.lang.annotation.*;
/**
* 自定義操作日志注解:標(biāo)記需要記錄日志的方法
*/
@Target({ElementType.METHOD}) // 僅作用于方法
@Retention(RetentionPolicy.RUNTIME) // 運(yùn)行時(shí)生效(AOP需動態(tài)獲取注解信息)
@Documented // 生成JavaDoc時(shí)包含該注解
public @interface OperationLog {
/** 操作模塊(如:用戶管理、訂單處理) */
String module() default "";
/** 操作描述(如:新增用戶、刪除訂單) */
String description() default "";
}
3.2 步驟2:定義日志實(shí)體類(SysOperationLog)
封裝日志的所有字段,與數(shù)據(jù)庫表sys_operation_log一一對應(yīng)(放在Common模塊)。
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作日志實(shí)體類:映射數(shù)據(jù)庫表sys_operation_log
*/
@Data // Lombok自動生成getter/setter/toString
public class SysOperationLog {
/** 日志ID(自增主鍵) */
private Long id;
/** 操作用戶(用戶名/賬號) */
private String username;
/** 操作時(shí)間 */
private LocalDateTime operationTime;
/** 操作模塊(如:用戶管理) */
private String module;
/** 操作描述(如:新增用戶) */
private String description;
/** 操作方法全路徑(如:com.example.service.UserService.addUser) */
private String method;
/** 方法參數(shù)(JSON格式) */
private Object params;
/** 操作結(jié)果(成功/失敗,JSON格式) */
private Object result;
/** 異常信息(失敗時(shí)記錄) */
private String exception;
/** 操作耗時(shí)(毫秒) */
private Long costTime;
/** 客戶端IP */
private String clientIp;
}
3.3 步驟3:定義日志處理接口(LogHandler)
為避免Common模塊直接依賴業(yè)務(wù)模塊(導(dǎo)致循環(huán)依賴),通過接口定義日志保存規(guī)范,業(yè)務(wù)模塊實(shí)現(xiàn)該接口(放在Common模塊)。
import com.wuxi.common.log.entity.SysOperationLog;
/**
* 日志處理接口:Common模塊定義規(guī)范,業(yè)務(wù)模塊實(shí)現(xiàn)具體邏輯
* 作用:解耦Common與業(yè)務(wù)模塊,避免循環(huán)依賴
*/
public interface LogHandler {
/**
* 保存操作日志
* @param sysOperationLog 日志實(shí)體
*/
void saveOperationLog(SysOperationLog sysOperationLog);
}
3.4 步驟4:實(shí)現(xiàn)AOP切面核心邏輯(OperationLogAspect)
AOP切面是日志收集的核心,負(fù)責(zé)攔截注解標(biāo)記的方法、收集日志信息、調(diào)用LogHandler保存日志(放在Common模塊)。
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.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;
/**
* 操作日志AOP切面:核心日志收集邏輯
*/
@Slf4j // Lombok自動生成日志對象
@Aspect // 標(biāo)記為AOP切面
@Component // 納入Spring容器管理
@RequiredArgsConstructor // Lombok自動生成構(gòu)造函數(shù),注入LogHandler
public class OperationLogAspect {
// 注入日志處理接口(業(yè)務(wù)模塊實(shí)現(xiàn)),避免依賴具體業(yè)務(wù)
private final LogHandler logHandler;
/**
* 定義切點(diǎn):攔截所有添加@OperationLog注解的方法
*/
@Pointcut("@annotation(com.wuxi.common.log.annotation.OperationLog)")
public void logPointCut() {}
/**
* 環(huán)繞通知:在方法執(zhí)行前后攔截,收集日志信息
* 優(yōu)勢:可獲取方法執(zhí)行前(如開始時(shí)間)、執(zhí)行后(如結(jié)果、耗時(shí))、異常信息
*/
@Around("logPointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 記錄方法開始時(shí)間(用于計(jì)算耗時(shí))
long startTime = System.currentTimeMillis();
// 2. 初始化日志實(shí)體
SysOperationLog logEntity = new SysOperationLog();
logEntity.setOperationTime(LocalDateTime.now()); // 操作時(shí)間
// 3. 獲取客戶端IP(從請求上下文獲?。?
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (requestAttributes != null) {
HttpServletRequest request = requestAttributes.getRequest();
logEntity.setClientIp(request.getRemoteAddr()); // 客戶端IP
}
// 4. 獲取方法信息(全路徑、參數(shù))
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 方法全路徑:包名+類名+方法名
logEntity.setMethod(method.getDeclaringClass().getName() + "." + method.getName());
// 方法參數(shù):數(shù)組轉(zhuǎn)字符串(后續(xù)需序列化為JSON)
logEntity.setParams(Arrays.toString(joinPoint.getArgs()));
// 5. 獲取@OperationLog注解信息(模塊、描述)
OperationLog operationLog = method.getAnnotation(OperationLog.class);
logEntity.setModule(operationLog.module());
logEntity.setDescription(operationLog.description());
// 6. 獲取操作用戶(從登錄上下文獲取,如Spring Security)
logEntity.setUsername(getCurrentUsername());
Object businessResult = null; // 業(yè)務(wù)方法返回結(jié)果
try {
// 執(zhí)行目標(biāo)業(yè)務(wù)方法(核心業(yè)務(wù)邏輯)
businessResult = joinPoint.proceed();
// 方法執(zhí)行成功:標(biāo)記結(jié)果
logEntity.setResult("成功");
// 若需記錄業(yè)務(wù)返回結(jié)果,可序列化后賦值:logEntity.setResult(JSON.toJSONString(businessResult));
} catch (Exception e) {
// 方法執(zhí)行失?。河涗洰惓P畔?
logEntity.setResult("失敗");
logEntity.setException(e.getMessage()); // 異常信息(簡化,可記錄堆棧)
throw e; // 重新拋出異常,不影響原有業(yè)務(wù)異常處理邏輯
} finally {
// 7. 計(jì)算操作耗時(shí)(結(jié)束時(shí)間-開始時(shí)間)
logEntity.setCostTime(System.currentTimeMillis() - startTime);
// 8. 保存日志(調(diào)用業(yè)務(wù)模塊實(shí)現(xiàn)的LogHandler)
saveOperationLog(logEntity);
}
// 返回業(yè)務(wù)方法結(jié)果,不影響業(yè)務(wù)流程
return businessResult;
}
/**
* 從登錄上下文獲取當(dāng)前用戶(實(shí)際項(xiàng)目需替換為真實(shí)邏輯)
* 示例:Spring Security可通過SecurityContextHolder獲取
*/
private String getCurrentUsername() {
// 模擬:從自定義UserContext獲取(實(shí)際項(xiàng)目需實(shí)現(xiàn)上下文管理)
String currentUser = UserContext.getUser();
// 若未獲取到用戶(如系統(tǒng)操作),默認(rèn)賦值為"system"
return currentUser == null ? "system" : currentUser;
}
/**
* 調(diào)用LogHandler保存日志,捕獲異常避免影響主業(yè)務(wù)
*/
private void saveOperationLog(SysOperationLog logEntity) {
try {
logHandler.saveOperationLog(logEntity);
} catch (Exception e) {
// 日志保存失敗不影響主業(yè)務(wù),僅記錄日志告警
log.error("記錄系統(tǒng)操作日志失敗,日志信息:{}", logEntity, e);
// 若日志為核心審計(jì)需求,可拋出自定義異常:throw new DbException("記錄日志失敗", e);
}
}
}
3.5 步驟5:數(shù)據(jù)庫表設(shè)計(jì)(sys_operation_log)
創(chuàng)建日志存儲表,字段與SysOperationLog實(shí)體對應(yīng),添加索引優(yōu)化查詢(如按時(shí)間、用戶查詢)。
CREATE TABLE `sys_operation_log` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志ID(自增主鍵)', `username` varchar(50) NOT NULL COMMENT '操作用戶', `operation_time` datetime NOT NULL COMMENT '操作時(shí)間', `module` varchar(100) NOT NULL COMMENT '操作模塊(如:用戶管理)', `description` varchar(255) DEFAULT NULL COMMENT '操作描述(如:新增用戶)', `method` varchar(255) NOT NULL COMMENT '操作方法全路徑', `params` text COMMENT '方法參數(shù)(JSON格式)', `result` text COMMENT '操作結(jié)果(成功/失敗,JSON格式)', `exception` text COMMENT '異常信息(失敗時(shí)記錄)', `cost_time` bigint DEFAULT NULL COMMENT '操作耗時(shí)(毫秒)', `client_ip` varchar(50) DEFAULT NULL COMMENT '客戶端IP', PRIMARY KEY (`id`), KEY `idx_operation_time` (`operation_time`) COMMENT '按操作時(shí)間查詢索引', KEY `idx_username` (`username`) COMMENT '按用戶查詢索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='系統(tǒng)操作日志表';
3.6 步驟6:實(shí)現(xiàn)LogHandler接口(業(yè)務(wù)模塊)
在業(yè)務(wù)模塊(如用戶服務(wù)、訂單服務(wù))中實(shí)現(xiàn)LogHandler接口,調(diào)用MyBatis將日志插入數(shù)據(jù)庫(避免Common模塊依賴業(yè)務(wù))。
import com.wuxi.common.log.entity.SysOperationLog;
import com.wuxi.common.log.handler.LogHandler;
import com.wuxi.user.mapper.SysOperationLogMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON; // 需引入FastJSON依賴
/**
* 日志處理實(shí)現(xiàn)類:業(yè)務(wù)模塊實(shí)現(xiàn),負(fù)責(zé)將日志插入數(shù)據(jù)庫
*/
@Component
@RequiredArgsConstructor
public class LogHandlerImpl implements LogHandler {
// 注入MyBatis Mapper(操作數(shù)據(jù)庫)
private final SysOperationLogMapper sysOperationLogMapper;
@Override
public void saveOperationLog(SysOperationLog sysOperationLog) {
// 關(guān)鍵:將Object類型的params/result序列化為JSON字符串(適配數(shù)據(jù)庫text類型)
if (sysOperationLog.getParams() != null) {
sysOperationLog.setParams(JSON.toJSONString(sysOperationLog.getParams()));
}
if (sysOperationLog.getResult() != null) {
sysOperationLog.setResult(JSON.toJSONString(sysOperationLog.getResult()));
}
// 調(diào)用MyBatis Mapper插入數(shù)據(jù)庫
sysOperationLogMapper.insert(sysOperationLog);
}
}
3.7 步驟7:MyBatis映射(插入日志)
編寫MyBatis Mapper接口和XML映射文件,實(shí)現(xiàn)日志插入邏輯。
Mapper接口
import com.wuxi.common.log.entity.SysOperationLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;
/**
* 操作日志MyBatis Mapper
*/
public interface SysOperationLogMapper {
/**
* 插入操作日志
* @Options:自增主鍵回寫(將數(shù)據(jù)庫生成的id賦值給實(shí)體類的id字段)
*/
@Insert("INSERT INTO sys_operation_log (" +
"username, operation_time, module, description, method, " +
"params, result, exception, cost_time, client_ip" +
") VALUES (" +
"#{username}, #{operationTime}, #{module}, #{description}, #{method}, " +
"#{params}, #{result}, #{exception}, #{costTime}, #{clientIp}" +
")")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(SysOperationLog sysOperationLog);
}
(可選)XML映射文件
若偏好XML配置,可替換為以下方式(SysOperationLogMapper.xml):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wuxi.user.mapper.SysOperationLogMapper">
<!-- 插入操作日志 -->
<insert id="insert" parameterType="com.wuxi.common.log.entity.SysOperationLog"
useGeneratedKeys="true" keyProperty="id">
INSERT INTO sys_operation_log (
username, operation_time, module, description, method,
params, result, exception, cost_time, client_ip
) VALUES (
#{username}, #{operationTime}, #{module}, #{description}, #{method},
#{params}, #{result}, #{exception}, #{costTime}, #{clientIp}
)
</insert>
</mapper>
四、實(shí)際使用示例
在業(yè)務(wù)方法上添加@OperationLog注解,即可自動記錄日志,無需額外編寫日志代碼。
import com.wuxi.common.log.annotation.OperationLog;
import com.wuxi.user.entity.User;
import com.wuxi.user.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
/**
* 新增用戶接口:添加@OperationLog注解,自動記錄日志
*/
@PostMapping("/user/add")
@OperationLog(module = "用戶管理", description = "新增用戶")
public String addUser(@RequestBody User user) {
userService.saveUser(user);
return "新增用戶成功";
}
/**
* 刪除用戶接口:記錄日志
*/
@PostMapping("/user/delete")
@OperationLog(module = "用戶管理", description = "刪除用戶")
public String deleteUser(Long userId) {
userService.deleteUser(userId);
return "刪除用戶成功";
}
}
五、關(guān)鍵問題與解決方案
5.1 如何避免循環(huán)依賴
問題:Common模塊需調(diào)用業(yè)務(wù)模塊的日志保存邏輯,若Common直接依賴業(yè)務(wù)模塊,會形成“Common→業(yè)務(wù)→Common”的循環(huán)依賴。
解決方案:接口解耦
- Common模塊定義
LogHandler接口,不依賴業(yè)務(wù); - 業(yè)務(wù)模塊實(shí)現(xiàn)
LogHandler接口,依賴Common模塊; - 最終依賴鏈:
業(yè)務(wù)模塊→Common模塊(單向依賴,無循環(huán))。
5.2 Object類型參數(shù)/結(jié)果如何存儲
問題:SysOperationLog的params和result是Object類型,數(shù)據(jù)庫是text類型,直接存儲會報(bào)錯(cuò)。
解決方案:JSON序列化
使用FastJSON/Jackson將Object序列化為JSON字符串,存儲到數(shù)據(jù)庫(如LogHandlerImpl中JSON.toJSONString())。
5.3 LocalDateTime與MySQL datetime映射問題
問題:Java 8的LocalDateTime與MySQL的datetime類型默認(rèn)不兼容,會報(bào)類型轉(zhuǎn)換錯(cuò)誤。
解決方案:
- 引入
mybatis-typehandlers-jsr310依賴(已在環(huán)境準(zhǔn)備中添加); - MyBatis自動識別該類型處理器,無需額外配置。
5.4 日志保存失敗影響主業(yè)務(wù)
問題:若數(shù)據(jù)庫異常導(dǎo)致日志保存失敗,不能阻斷核心業(yè)務(wù)流程。
解決方案:異常隔離
在AOP的saveOperationLog方法中捕獲異常,僅記錄告警日志,不拋出異常(除非是核心審計(jì)日志,需強(qiáng)制記錄)。
六、方案優(yōu)化方向
- 異步保存日志:通過
@Async注解異步執(zhí)行日志保存,避免日志操作阻塞主業(yè)務(wù)(需開啟Spring異步支持@EnableAsync); - 日志脫敏:對敏感參數(shù)(如密碼、手機(jī)號)進(jìn)行脫敏處理后再存儲(如用
***替換中間字符); - 日志分表:日志數(shù)據(jù)量較大時(shí),按時(shí)間分表(如每月一張表),提升查詢性能;
- 分布式日志:微服務(wù)場景下,可將日志發(fā)送到ELK(Elasticsearch+Logstash+Kibana),實(shí)現(xiàn)日志集中查詢與分析。
七、總結(jié)
本文基于AOP+Spring實(shí)現(xiàn)的操作日志方案,核心優(yōu)勢在于:
- 解耦:日志邏輯與業(yè)務(wù)邏輯完全分離,無侵入;
- 可擴(kuò)展:新增日志字段或修改保存邏輯,只需調(diào)整切面或LogHandler實(shí)現(xiàn);
- 易用性:業(yè)務(wù)方法只需添加注解,即可自動記錄日志。
該方案適用于單體應(yīng)用和微服務(wù)架構(gòu),可根據(jù)實(shí)際需求擴(kuò)展異步、脫敏、分表等功能,是企業(yè)級系統(tǒng)操作日志的最佳實(shí)踐之一。
以上就是SpringBoot使用AOP優(yōu)雅實(shí)現(xiàn)系統(tǒng)操作日志的持久化的詳細(xì)內(nèi)容,更多關(guān)于SpringBoot AOP日志持久化的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot集成Spring Security的方法
Spring security,是一個(gè)強(qiáng)大的和高度可定制的身份驗(yàn)證和訪問控制框架。這篇文章主要介紹了SpringBoot集成Spring Security的操作方法,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-07-07

