今天就来讲讲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日志记录的资料请关注代码网其它相关文章!
发表评论