SpringBoot实现操作日志记录完整指南

引言

在项目中,无论是在开发环境还是生产环境,如果系统出现故障,迎面而来的第一个问题往往是: "这个操作是谁在什么时候执行的?具体做了什么改动?" 如果系统对此一无所知,排查工作就如同大海捞针。

操作日志记录正是为了解决这一问题而生。它是一个系统性的、用于追踪用户行为、厘清操作责任以及复现历史流程的核心功能。它是系统的"黑匣子",也是开发者的"记事本"。 在SpringBoot项目中,若将日志记录代码散乱地嵌入到每个业务方法的try-catch块中,会导致代码重复严重、核心业务逻辑被污染、不利于维护。

本文将探讨如何利用SpringBoot面向切面编程(AOP) 来优雅地解决这一难题。

一、环境准备与项目搭建

1. 创建一个标准的SpringBoot项目

我使用的是SpringBoot3.5.5+JDK17

2. 引入核心依赖

  • mysql-connector-j(数据库驱动)
  • mybatis-plus-spring-boot3-starter(数据持久化框架)
  • spring-boot-starter-aop(AOP核心)
  • aspectjweaver(面向切面工具)
  • spring-boot-starter-web(Web项目)
  • fastjson2(阿里json解析器)

二、实现步骤

1. 创建日志实体类(OperateLog)

java 复制代码
@Data
public class OperateLog {
    private Long id; //主键
    private Long userId; //操作人id
    private Date createTime; //创建时间
    private String description; //请求描述
    private String method; //请求方法 如:post、get
    private String ip; //操作人ip
    private String param; //请求参数:json格式
    private String result; //请求结果:json格式
    private Integer success; //操作是否成功
    private String errorMsg; //错误信息
}

2. 创建操作日志记录表(operate_log)

sql 复制代码
create table operate_log(
	id BIGINT PRIMARY key AUTO_INCREMENT COMMENT '主键',
	user_id BIGINT DEFAULT 100 COMMENT '操作人id',
	create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
	description VARCHAR(200) COMMENT '请求描述',
	method VARCHAR(20) COMMENT '请求方法 如:post、get',
	ip VARCHAR(32) COMMENT '操作人ip',
	param VARCHAR(2000) COMMENT '请求参数:json格式',
	result VARCHAR(2000) COMMENT '请求结果:json格式',
	success TINYINT DEFAULT 0 COMMENT '操作是否成功:1是、0否',
	error_msg VARCHAR(2000) COMMENT '错误消息'
) COMMENT '操作日志记录表';

3. 创建添加日志的相关代码

java 复制代码
//mapper接口
public interface OperateLogMapper extends BaseMapper<OperateLog> {}

//service接口
public interface OperateLogService extends IService<OperateLog> {}

//service实现类
@Service
public class OperateLogServiceImpl extends ServiceImpl<OperateLogMapper, OperateLog> implements OperateLogService {}

4. 创建自定义注解(@Log)

在注解类中,指定其可以对方法进行修饰,并设置运行时保留策略

java 复制代码
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 请求描述
     */
    String title() default "";
}

5. 创建切面类(LogAspect)- 核心

在切面类中,创建前置通知和后置通知,并指定被@Log注解修饰的方法为切点,实现对目标方法的"增强"。

java 复制代码
@Aspect
@Component
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    @Autowired
    private OperateLogService logService;

    private final ExecutorService executor = Executors.newSingleThreadExecutor();

    /**
     * 请求前执行
     * @param aspLog
     */
    @Before(value = "@annotation(aspLog)")
    public void doBefore(Log aspLog){
        log.info("请求日志记录start");
    }

    /**
     * 请求后执行
     * @param aspLog
     * @param result
     */
    @AfterReturning(pointcut = "@annotation(aspLog)",returning = "result")
    public void doAfterReturning(JoinPoint joinPoint, Log aspLog, Object result){
        handleLog(joinPoint,aspLog,null,result);
    }

    /**
     * 请求异常执行
     * @param aspLog
     * @param e
     */
    @AfterThrowing(value = "@annotation(aspLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint,Log aspLog,Exception e){
        handleLog(joinPoint,aspLog,e,null);
    }

    /**
     * 日志处理器-记录日志
     * @param aspLog
     * @param e
     * @param result
     */
    private void handleLog(JoinPoint joinPoint,Log aspLog,Exception e,Object result){
        ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        OperateLog operateLog = new OperateLog();
        operateLog.setIp(attributes.getRequest().getRemoteAddr());
        operateLog.setMethod(attributes.getRequest().getMethod());
        String param = JSON.toJSONString(joinPoint.getArgs());
        operateLog.setParam(param);
        if(e != null){
            operateLog.setErrorMsg(e.getLocalizedMessage());
        }else{
            operateLog.setSuccess(1);
            operateLog.setResult(JSON.toJSONString(result));
        }
        operateLog.setDescription(aspLog.title());
        executor.submit(() -> logService.save(operateLog));
    }
}

6. 编写控制层代码进行测试

java 复制代码
@RestController
public class TestController {
    @Log(title = "测试操作")
    @GetMapping("/test")
    public String test(){
        return "没毛病";
    }

    @Log(title = "测试参数操作")
    @PostMapping("/testParam")
    public String testParam(@RequestBody String data){
        return data;
    }

    @Log(title = "测试异常操作")
    @GetMapping("/testExecption")
    public String testExecption(){
        return (1/0)+"";
    }
}

7. 其他注意事项

如果你使用的Springboot也是3.0+,在引入mybatis-plus依赖的时候,一定要导入mybatis-plus-spring-boot3-starter,而不是mybatis-plus-boot-starter。如果导入的依赖不正确,会导致Spring无法注入mybatis相关的内容并报错。

三、功能测试与验证(Postman)

测试三种情况,1、测试无参get请求,2、测试传参post请求,3、测试异常请求,对于成功的请求要在日志记录中标记为成功,对于异常的请求要记录异常信息。

1. 测试无参get请求

2. 测试post带参数请求

3. 测试异常请求

四、 总结

到这里,就完成了操作日志的记录功能,需要注意的是,在日常开发中,一定要将记录日志的代码交给另一个线程执行,避免持久化操作对主线程产生性能上的影响。面向切面的本质上是对切点方法做了一层代理,在不影响业务代码的前提下对其进行功能扩展,降低了业务代码和非业务代码的耦合度,这种设计适用于很多其他的业务场景,比如异常处理、接口性能监控、参数校验与预处理等。

相关推荐
仰望星空@脚踏实地36 分钟前
maven scope 详解
java·maven·scope
bobz96539 分钟前
linux cpu CFS 调度器有使用 令牌桶么?
后端
bobz96543 分钟前
linux CGROUP CPU 限制有使用令牌桶么?
后端
M_Reus_111 小时前
Groovy集合常用简洁语法
java·开发语言·windows
带刺的坐椅1 小时前
10分钟带你体验 Solon 的状态机
java·solon·状态机·statemachine
小鹅叻1 小时前
MyBatis题
java·tomcat·mybatis
RainbowSea1 小时前
4. LangChain4j 模型参数配置超详细说明
java·langchain·ai编程
RainbowSea1 小时前
3. LangChain4j + 低阶 和 高阶 API的详细说明
java·llm·ai编程
叫我阿柒啊1 小时前
Java全栈开发面试实战:从基础到微服务的深度探索
java·spring boot·redis·微服务·vue3·全栈开发·面试技巧
ashane13141 小时前
Springboot 集成 TraceID
java·spring boot·spring