当前位置: 代码网 > it编程>编程语言>Java > 基于SpringBoot+AOP实现操作日志记录

基于SpringBoot+AOP实现操作日志记录

2026年04月17日 Java 我要评论
今天就来讲讲spring aop最实用的实战场景——用 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日志记录的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com