基于SpringBoot+AOP实现操作日志记录
今天就来讲讲Spring AOP最实用的实战场景——用 SpringBoot + AOP 实现操作日志记录。
操作日志是项目必备功能:比如用户登录、接口调用、数据新增/修改/删除,都需要记录操作人、操作时间、操作内容、接口地址等信息,方便后续排查问题、审计追溯。
一、明确操作日志要记录哪些信息?
先梳理操作日志的核心字段,避免后续代码遗漏,实战中可根据项目需求增减,这里给出通用模板:
- 操作人:当前登录用户的用户名/ID(实战中结合 Spring Security 或 Token 获取);
- 操作时间:接口执行的时间(精确到毫秒);
- 操作模块:比如“用户管理”“订单管理”“商品管理”(标记当前操作属于哪个模块);
- 操作描述:比如“新增用户”“删除订单”“查询商品列表”(清晰说明操作内容);
- 接口地址:被调用的接口 URL(比如 /api/user/add);
- 请求方式:GET/POST/PUT/DELETE;
- 请求参数:接口接收的参数(JSON 格式);
- 返回结果:接口返回的数据(JSON 格式);
- 执行状态:成功/失败;
- 异常信息:如果接口执行失败,记录异常详情(便于排查);
- 操作 IP:调用接口的客户端 IP 地址。
二、从零实现 AOP 操作日志
步骤1:搭建基础环境(导入依赖)
SpringBoot 项目中,实现 AOP 只需导入 spring-boot-starter-aop 依赖,无需额外导入其他包,在 pom.xml 中添加:
<!-- Spring AOP 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 工具包:用于 JSON 格式化、IP 地址获取(可选,简化代码) -->
<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 用于将请求参数、返回结果转为 JSON 字符串;UserAgentUtils 用于获取客户端 IP 和浏览器信息,可根据需求选择是否导入。
步骤2:创建操作日志实体类(存储日志数据)
创建实体类 OperationLog,对应操作日志的核心字段,后续可直接映射到数据库(这里省略数据库操作,重点放在 AOP 实现):
import lombok.Data;
import java.time.LocalDateTime;
/**
* 操作日志实体类
*/
@Data
public class OperationLog {
// 主键(实战中可自增)
private Long id;
// 操作人(用户名/ID)
private String operator;
// 操作时间
private LocalDateTime operationTime;
// 操作模块
private String module;
// 操作描述
private String description;
// 接口地址
private String requestUrl;
// 请求方式
private String requestMethod;
// 请求参数(JSON 格式)
private String requestParams;
// 返回结果(JSON 格式)
private String responseResult;
// 执行状态(0-失败,1-成功)
private Integer status;
// 异常信息(失败时填写)
private String errorMsg;
// 操作 IP
private String operationIp;
}说明:用 @Data 注解(lombok)简化 getter/setter 方法,实战中需导入 lombok 依赖(如果未导入)。
步骤3:创建自定义注解(精准定位需要记录日志的接口)
我们用「自定义注解」来标记需要记录操作日志的接口,这样可以灵活控制哪些接口需要记录日志,哪些不需要——比直接用切点表达式匹配包/类更灵活。
import java.lang.annotation.*;
/**
* 自定义操作日志注解
* @Target:注解作用范围(METHOD:作用在方法上)
* @Retention:注解保留时机(RUNTIME:运行时保留,AOP 可获取)
* @Documented:生成文档时包含该注解
*/
@Target(ElementType.METHOD) // 只作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface OperationLogAnnotation {
// 操作模块(必填,比如“用户管理”)
String module() default "";
// 操作描述(必填,比如“新增用户”)
String description() default "";
}说明:该注解有两个属性,module(操作模块)和 description(操作描述),在需要记录日志的接口方法上添加该注解,并填写对应属性即可。
步骤4:创建 AOP 切面(实现日志记录)
这是本次实战的核心,创建切面类,定义切点(匹配带有 @OperationLogAnnotation 注解的方法)、通知(环绕通知,实现日志记录逻辑),完成日志的收集和处理。
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:标记此类为切面
* @Component:交给 Spring 管理,让 Spring 扫描到该切面
*/
@Aspect
@Component
public class OperationLogAspect {
// 1. 定义切点:匹配带有 @OperationLogAnnotation 注解的方法
@Pointcut("@annotation(com.example.demo.annotation.OperationLogAnnotation)")
public void operationLogPointcut() {} // 切点方法,无实际逻辑,仅用于标记
// 2. 定义环绕通知:包裹切点方法,可在方法执行前、执行后、异常时处理
@Around("operationLogPointcut()")
public Object recordOperationLog(ProceedingJoinPoint joinPoint) throws Throwable {
// 1. 初始化操作日志对象
OperationLog operationLog = new OperationLog();
// 2. 获取当前请求对象,用于获取请求信息(URL、请求方式、IP 等)
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 3. 填充日志基础信息(无论接口成功/失败,都需要记录)
// 3.1 获取操作人(实战中需结合 Spring Security/Token 获取,这里模拟 admin)
operationLog.setOperator("admin");
// 3.2 操作时间(当前时间)
operationLog.setOperationTime(LocalDateTime.now());
// 3.3 接口地址
operationLog.setRequestUrl(request.getRequestURI());
// 3.4 请求方式(GET/POST)
operationLog.setRequestMethod(request.getMethod());
// 3.5 操作 IP(获取客户端真实 IP)
operationLog.setOperationIp(getClientIp(request));
// 3.6 请求参数(将方法参数转为 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. 执行目标方法(核心业务逻辑),捕获执行结果和异常
Object result = null;
try {
// 执行目标方法(比如接口的核心逻辑)
result = joinPoint.proceed();
// 方法执行成功:设置状态为 1(成功),记录返回结果
operationLog.setStatus(1);
operationLog.setResponseResult(JSON.toJSONString(result));
} catch (Throwable throwable) {
// 方法执行失败:设置状态为 0(失败),记录异常信息
operationLog.setStatus(0);
operationLog.setErrorMsg(throwable.getMessage());
// 抛出异常,不影响原有业务逻辑的异常处理
throw throwable;
} finally {
// 6. 日志持久化(实战中可存入数据库、ElasticSearch 等,这里模拟打印)
System.out.println("操作日志记录:" + JSON.toJSONString(operationLog, true));
// TODO: 实战中替换为数据库插入操作(比如调用 OperationLogService.save(operationLog))
}
// 返回目标方法的执行结果,不影响原有接口的返回值
return result;
}
/**
* 工具方法:获取客户端真实 IP(处理代理场景,比如 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();
}
// 处理多代理场景,取第一个非 unknown 的 IP
if (ip != null && ip.contains(",")) {
ip = ip.split(",")[0].trim();
}
return ip;
}
}核心解读:
- 切点:通过 @annotation 匹配带有自定义注解的方法,精准控制需要记录日志的接口;
- 环绕通知:用 @Around 包裹目标方法,先收集请求信息、注解属性,再执行目标方法,最后处理日志(成功/失败);
- IP 获取:处理了 Nginx 代理等场景,确保获取到客户端真实 IP;
- 异常处理:捕获目标方法的异常,记录异常信息,同时重新抛出异常,不影响原有业务的异常处理逻辑;
- 日志持久化:这里用打印模拟,实战中需替换为数据库插入、ES 存储等逻辑。
步骤5:接口测试(验证日志记录效果)
创建一个测试接口,添加自定义 @OperationLogAnnotation 注解,启动项目,调用接口,查看日志是否正常记录。
import com.example.demo.annotation.OperationLogAnnotation;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
/**
* 测试接口:用户管理模块
*/
@RestController
@RequestMapping("/api/user")
public class UserController {
// 添加 @OperationLogAnnotation 注解,标记需要记录日志
@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;
}
// 测试异常场景
@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;
}
}测试1:调用新增用户接口
请求地址:http://localhost:8080/api/user/add
请求方式:POST
请求参数:{"username":"test","password":"123456"}
控制台打印的日志(格式化后):
操作日志记录:{
"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
}测试2:调用删除用户接口
请求地址:http://localhost:8080/api/user/delete/-1
请求方式:DELETE
控制台打印的日志(格式化后):
操作日志记录:{
"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
}验证结果:两种场景的日志都正常记录,包含了所有核心字段,符合预期!
三、优化技巧
上面的基础实现已经能满足大部分项目需求,下面补充3个实战常用的优化点,让日志功能更完善。
优化1:获取真实操作人(替换模拟值)
实战中,操作人不能用模拟的“admin”,需结合 Spring Security 或 Token 解析获取当前登录用户:
// 结合 Spring Security 获取当前登录用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && !(authentication.getPrincipal() instanceof String)) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
operationLog.setOperator(userDetails.getUsername()); // 获取用户名
}优化2:日志持久化(存入数据库)
创建 OperationLogService 和 OperationLogMapper,将日志对象存入数据库(以 MyBatis-Plus 为例):
// 1. 注入 OperationLogService
@Autowired
private OperationLogService operationLogService;
// 2. 在 finally 中替换打印逻辑,改为存入数据库
finally {
operationLogService.save(operationLog); // MyBatis-Plus 自带的保存方法
}优化3:忽略敏感参数(避免日志泄露)
接口参数中可能包含密码、手机号等敏感信息,需要忽略这些参数,避免日志泄露,可自定义注解+拦截处理:
// 1. 自定义忽略敏感参数注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreSensitive {
}
// 2. 在实体类敏感字段上添加注解
@Data
public class User {
private Long id;
private String username;
@IgnoreSensitive // 忽略密码字段
private String password;
}
// 3. 在切面中,处理敏感参数(替换为 ****)
// (核心逻辑:反射获取字段,判断是否有 @IgnoreSensitive 注解,有则替换值)四、注意事项
切面类忘记加 @Component 注解
❌ 错误做法:只加 @Aspect 标记切面,忘记加 @Component;
✅ 正确做法:@Aspect 只是标记切面,必须加 @Component 交给 Spring 管理,否则 Spring 无法扫描到切面,日志记录失效。
环绕通知中忘记调用 joinPoint.proceed()
❌ 错误做法:只收集日志,不执行目标方法,导致接口无法正常返回;
✅ 正确做法:必须调用 joinPoint.proceed() 执行目标方法,同时接收返回结果,否则核心业务逻辑无法执行。
请求参数为 MultipartFile(文件上传)时,JSON 格式化报错
❌ 错误表现:文件上传接口,日志记录时,JSON.toJSONString(args) 报错;
✅ 解决方案:判断参数类型,如果是 MultipartFile,不进行 JSON 格式化,直接标记为“文件上传”。
文末小结
用 SpringBoot + AOP 实现操作日志,核心就是“自定义注解标记接口 + 切面收集日志信息 + 环绕通知处理增强”,全程无侵入式编码,复用性极高。
记住:AOP 的核心是“解耦”,把日志这种通用功能,和核心业务逻辑分离,既保证了核心代码的简洁,又方便后续维护和扩展。
以上就是基于SpringBoot+AOP实现操作日志记录的详细内容,更多关于SpringBoot AOP日志记录的资料请关注脚本之家其它相关文章!
相关文章
Spring5+SpringMvc+Hibernate5整合的实现
这篇文章主要介绍了Spring5+SpringMvc+Hibernate5整合的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧2020-06-06
springboot 中 inputStream 神秘消失之谜(终破)
这篇文章主要介绍了springboot 中 inputStream 神秘消失之谜,为了能够把这个问题说明,我们首先需要从简单的http调用说起,通过设置body等一些操作,具体实现代码跟随小编一起看看吧2021-08-08
在Java的Spring框架的程序中使用JDBC API操作数据库
这篇文章主要介绍了在Java的Spring框架的程序中使用JDBC API操作数据库的方法,并通过示例展示了其存储过程以及基本SQL语句的应用,需要的朋友可以参考下2015-12-12


最新评论