java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > SpringBoot AOP日志记录

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

作者:希望永不加班

本文介绍了使用日志记录的实现方法,步骤包括明确记录信息、搭建基础环境、创建操作日志实体类和自定义注解、创建AOP切面,重点在于精准控制需要记录日志的接口,通过自定义注解和使用、切点表达式、环绕通知实现实现日志记录逻辑,需要的朋友可以参考下

今天就来讲讲Spring AOP最实用的实战场景——用 SpringBoot + AOP 实现操作日志记录

操作日志是项目必备功能:比如用户登录、接口调用、数据新增/修改/删除,都需要记录操作人、操作时间、操作内容、接口地址等信息,方便后续排查问题、审计追溯。

一、明确操作日志要记录哪些信息?

先梳理操作日志的核心字段,避免后续代码遗漏,实战中可根据项目需求增减,这里给出通用模板:

二、从零实现 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;
    }
}

核心解读:

步骤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日志记录的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文