当前位置: 首页 > article >正文

SpringBoot 项目实现操作日志的记录(使用 AOP 注解模式)


本文是博主在做关于如何记录用户操作日志时做的记录,常见的项目中难免存在一些需要记录重要日志的部分,例如权限和角色设定,重要数据的操作等部分。

博主使用 Spring 中的 AOP 功能,结合注解的方式,对用户操作过的一些重要方法做日志记录,存储到数据库中,可以随时查阅。

本文使用技术:SpringBoot 3 + JDK17 + MyBatis-Plus + MySQL。


文章目录

    • 01、PostMan 测试结果
    • 02、数据库存储数据后展示样式
    • 03、数据库表创建语句
    • 04、项目结构图
    • 05、代码:application.yml
    • 06、代码:pom.xml
    • 07、代码:注解 LogRecord
    • 08、代码:LogRecordAop
    • 09、代码:OperateTypeEnum
    • 10、代码:TestController
    • 11、代码:LogRecordMapper(使用 MyBatis-Plus)
    • 12、代码:ProjectLog(实体类)
    • 13、代码:UserInfo
    • 14、代码:UserServiceImpl
    • 15、代码:IUserService
    • 16、代码:LogAopUtils
    • 17、代码:LogHolder
    • 18、代码:UserHolder
    • 19、代码:Application
    • 20、代码:LogRecordMapper.xml

01、PostMan 测试结果

青灯文案-测试

02、数据库存储数据后展示样式

在这里插入图片描述

03、数据库表创建语句

CREATE TABLE project_log (id bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志ID',company_id varchar(36) DEFAULT NULL COMMENT '公司ID',company varchar(200) DEFAULT NULL COMMENT '公司',uname varchar(100) DEFAULT NULL COMMENT '用户名',uid bigint(20) DEFAULT NULL COMMENT '用户ID',operate_type int(10) DEFAULT NULL COMMENT '操作类型',operate_module varchar(64) DEFAULT NULL COMMENT '操作模块',operate_desc varchar(255) DEFAULT NULL COMMENT '操作描述',operate_param text DEFAULT NULL COMMENT '操作参数',operate_time bigint(20) DEFAULT NULL COMMENT '操作时间',operate_result tinyint(10) DEFAULT NULL COMMENT '操作结果',PRIMARY KEY (id)
);create index idx_operate_typeon project_log (operate_type);create index idx_operate_moduleon project_log (operate_module);create index idx_uidon project_log (uid);create index idx_companyon project_log (company);create index idx_unameon project_log (uname);

04、项目结构图

青灯文案-项目结构

05、代码:application.yml

server:port: 8089spring:datasource:url: jdbc:mysql://10.100.4.163:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: 123456

06、代码:pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.wen</groupId><artifactId>Test-Operate-Log</artifactId><version>1.0-SNAPSHOT</version><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.4.1</version><relativePath/> <!-- lookup parent from repository --></parent><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-jsqlparser</artifactId><version>3.5.9</version></dependency><!--mybatis--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-spring-boot3-starter</artifactId><version>3.5.9</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-core</artifactId><version>1.3.5</version><scope>test</scope></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><dependency><groupId>com.fhs-opensource</groupId><artifactId>easy-trans-mybatis-plus-extend</artifactId><version>3.0.6</version></dependency><dependency><groupId>com.oracle.database.jdbc</groupId><artifactId>ojdbc8</artifactId></dependency><dependency><groupId>org.mybatis.dynamic-sql</groupId><artifactId>mybatis-dynamic-sql</artifactId><version>1.4.0</version></dependency><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.1</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-jar-plugin</artifactId><configuration><archive><!--生成的jar 不要包含pom.xml pom.properties 这2个文件--><addMavenDescriptor>false</addMavenDescriptor><manifest><!--是否要不第三番jar放到manifest的classpath中--><addClasspath>true</addClasspath><!--生成的manifest中的classpath的前缀,因为要把第三方jar放到lib目录下,所以classpath前缀是lib/--><classpathPrefix>lib/</classpathPrefix><!--应用的main class --><mainClass>cn.com.wind.server.Application</mainClass></manifest></archive></configuration></plugin></plugins></build></project>

07、代码:注解 LogRecord

package com.wen.common;import com.wen.common.OperateTypeEnum;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** @author : rjw* @date : 2025-05-19*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {OperateTypeEnum operateType() default OperateTypeEnum.OTHER;
}

08、代码:LogRecordAop

package com.wen.common;import cn.hutool.core.util.ArrayUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.wen.mapper.LogRecordMapper;
import com.wen.model.UserInfo;
import com.wen.model.ProjectLog;
import com.wen.service.IUserService;
import com.wen.utils.LogAopUtils;
import com.wen.utils.LogHolder;
import com.wen.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.Arrays;/*** @author : rjw* @date : 2025-05-07*/
@Aspect
@Component
@Slf4j
public class LogRecordAop {@Autowiredprivate IUserService userService;@Autowiredprivate LogRecordMapper logRecordMapper;/*** 切入点 使用注解路径*/@Pointcut("@annotation(com.wen.common.LogRecord)")public void record() {}/*** 前置操作* @param point 切入点*/@Before("record() && @annotation(logRecord)")public void before(JoinPoint point, LogRecord logRecord) {ProjectLog logInfo = new ProjectLog();UserInfo user = UserHolder.getCurrentUser();// id 自增if (user != null) {logInfo.setUid(user.getUid());logInfo.setUname(user.getName());logInfo.setCompanyId(user.getCompanyId() == null ? "" : user.getCompanyId());logInfo.setCompany(user.getCompany());} else {// 如果为null,则说明开发人员使用接口调用logInfo.setUid(-1L);logInfo.setUname("开发人员");logInfo.setCompanyId("");logInfo.setCompany("tx");}logInfo.setOperateType(logRecord.operateType().getCode());logInfo.setOperateModule(logRecord.operateType().getModule());logInfo.setOperateTime(System.currentTimeMillis());Object[] args = LogAopUtils.removeUnnecessaryArgs(point.getArgs());logInfo.setOperateDesc(genOperateDesc(logRecord.operateType(), args, logInfo));if (ArrayUtil.isNotEmpty(args)) {if (logRecord.operateType() == OperateTypeEnum.EXPORT) {logInfo.setOperateParam(JSON.toJSONString(args[0]));} else {logInfo.setOperateParam(JSON.toJSONString(args));}}LogHolder.setProjectLog(logInfo);}private String genOperateDesc(OperateTypeEnum operateType, Object[] args, ProjectLog projectLog) {String operateDesc = String.format("【%s】-%s", operateType.getModule(), operateType.getFunction());if (ArrayUtil.isEmpty(args)) {return operateDesc;}switch (operateType) {case OPERATE:operateDesc = String.format(operateDesc, Arrays.toString(args));break;default:JSONObject req = JSONObject.parseObject(JSON.toJSONString(args[0]));operateDesc = String.format(operateDesc, req.toJSONString());break;}log.info("operateDesc: {}", operateDesc);return operateDesc;}@AfterReturning(value = "record()", returning = "response")public void afterReturning(Object response) {try {ProjectLog logInfo = LogHolder.getProjectLog();//   ======== 这里要求要使用返回值   if (logInfo == null || response == null) {return;}logInfo.setOperateResult((byte) 400);if (!checkParam(logInfo)) {log.warn("add log to DB failed cause param error! complianceLog:{}", logInfo);return;}addPrivateQuoteLog(logInfo);} finally {LogHolder.removeProjectLog();}}@AfterThrowing(value = "record()", throwing = "t")public void afterThrowing(Throwable t) {try {ProjectLog infoLog = LogHolder.getProjectLog();if (infoLog == null) {return;}infoLog.setOperateResult((byte) 400);if (!checkParam(infoLog)) {log.warn("add log to DB failed cause param error! complianceLog:{}", infoLog);return;}addPrivateQuoteLog(infoLog);} finally {LogHolder.getProjectLog();}}@Asyncprivate void addPrivateQuoteLog(ProjectLog projectLog) {logRecordMapper.insert(projectLog);}private boolean checkParam(ProjectLog projectLog) {return projectLog != null &&projectLog.getCompanyId() != null &&projectLog.getCompany() != null &&projectLog.getUid() != null &&projectLog.getOperateResult() != null &&projectLog.getOperateTime() != null &&projectLog.getOperateType() != null &&projectLog.getUname() != null;}
}

09、代码:OperateTypeEnum

package com.wen.common;import lombok.Getter;/*** @author : rjw* @date : 2025-05-19*/
@Getter
public enum OperateTypeEnum {OTHER(0, "系统","其它类型"),OPERATE(1, "内部接口—操作", "操作"),EXPORT(2, "内部接口—导出", "导出"),MAX(100, "", "");private final int code;private final String module;private final String function;OperateTypeEnum(int code, String module, String function) {this.code = code;this.module = module;this.function = function;}
}

10、代码:TestController

package com.wen.controller;import com.wen.common.LogRecord;
import com.wen.common.OperateTypeEnum;
import com.wen.model.UserInfo;
import com.wen.service.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;/*** @author : rjw* @date : 2025-05-19*/
@Slf4j
@RestController
@RequestMapping("/system")
public class TestController {@Autowiredprivate IUserService userService;@GetMapping("/testLog")@LogRecord(operateType = OperateTypeEnum.OPERATE)public UserInfo updateQuoteChannelPush(@RequestParam("id") long id, @RequestParam("name") String name,@RequestParam("company") String company, @RequestParam("companyId") String companyId) {userService.getCurrentUser();// 和内部数据无关UserInfo userInfo = new UserInfo();userInfo.setUid(id);userInfo.setName(name);userInfo.setCompany(company);userInfo.setCompanyId(companyId);log.info("userInfo is {}", userInfo);// 要有返回值,不然不能记录,可以在AOP中自己调return userInfo;}
}

11、代码:LogRecordMapper(使用 MyBatis-Plus)

package com.wen.mapper;import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.wen.model.ProjectLog;/*** @author : rjw* @date : 2025-05-19*/
public interface LogRecordMapper extends BaseMapper<ProjectLog> {}

12、代码:ProjectLog(实体类)

package com.wen.model;import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;/*** @author : rjw* @date : 2025-05-19*/
@Data
@TableName("project_log")
public class ProjectLog {/*** 日志 ID*/@TableId(value = "id", type = IdType.AUTO)private Long logId;/*** 公司ID*/private String companyId;/*** 公司*/private String company;/*** 用户名*/private String uname;/*** 用户ID*/private Long uid;/*** 操作类型*/private Integer operateType;/*** 操作模块*/private String operateModule;/*** 操作描述*/private String operateDesc;/*** 操作参数*/private String operateParam;/*** 操作时间*/private Long operateTime;/*** 操作结果*/private Byte operateResult;
}

13、代码:UserInfo

package com.wen.model;import lombok.Data;/*** @author : rjw* @date : 2025-05-19*/
@Data
public class UserInfo {private long uid;private String name;private String company;private String companyId;
}

14、代码:UserServiceImpl

package com.wen.service.impl;import com.wen.model.UserInfo;
import com.wen.service.IUserService;
import com.wen.utils.UserHolder;
import org.springframework.stereotype.Service;/*** @author : rjw* @date : 2025-05-19*/
@Service
public class UserServiceImpl implements IUserService {@Overridepublic UserInfo getCurrentUser() {/*** 一般是登陆的时候设置当前用户* 下线的时候删除当前用户*/UserInfo userInfo = new UserInfo();userInfo.setUid(1000001);userInfo.setName("张三");userInfo.setCompany("tx");userInfo.setCompanyId("tx");UserHolder.setCurrentUser(userInfo);return userInfo;}
}

15、代码:IUserService

package com.wen.service;import com.wen.model.UserInfo;/*** @author : rjw* @date : 2025-05-19*/
public interface IUserService {UserInfo getCurrentUser();}

16、代码:LogAopUtils

package com.wen.utils;import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.catalina.connector.RequestFacade;
import org.apache.catalina.connector.ResponseFacade;
import org.springframework.web.multipart.MultipartFile;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;public class LogAopUtils {public final static Set<Class<?>> UNNECESSARY_CLASS = new HashSet<Class<?>>(){{add(ServletRequest.class);add(ServletResponse.class);add(HttpServletRequest.class);add(HttpServletResponse.class);add(RequestFacade.class);add(ResponseFacade.class);}};public final static Set<Class<?>> UNNECESSARY_SUPER_CLASSES = new HashSet<Class<?>>() {{add(MultipartFile.class);}};public static Object[] removeUnnecessaryArgs(Object[] args) {List<Object> list = new ArrayList<>();outer: for (Object arg : args) {if (UNNECESSARY_CLASS.contains(arg.getClass())) {continue;}for (Class<?> superClass : UNNECESSARY_SUPER_CLASSES) {if(superClass.isAssignableFrom(arg.getClass())) {continue outer;}}list.add(arg);}return list.toArray();}
}

17、代码:LogHolder

package com.wen.utils;import com.wen.model.ProjectLog;public class LogHolder {private static final ThreadLocal<ProjectLog> LOCAL_PROJECT_LOG = new ThreadLocal<>();public static ProjectLog getProjectLog() {return LOCAL_PROJECT_LOG.get();}public static void setProjectLog(ProjectLog user) {LOCAL_PROJECT_LOG.set(user);}public static void removeProjectLog() {LOCAL_PROJECT_LOG.remove();}
}

18、代码:UserHolder

package com.wen.utils;import com.wen.model.UserInfo;/*** @author : rjw* @date : 2025-05-07*/
public class UserHolder {private static final ThreadLocal<UserInfo> CURRENT_USER = new ThreadLocal<>();public static UserInfo getCurrentUser() {return CURRENT_USER.get();}public static void setCurrentUser(UserInfo user) {CURRENT_USER.set(user);}public static void removeCurrentUser() {CURRENT_USER.remove();}
}

19、代码:Application

package com.wen;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;/*** @author : rjw* @date : 2025-05-19*/
@SpringBootApplication
@MapperScan(basePackages = {"com.wen.mapper"})
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class);}
}

20、代码:LogRecordMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.wen.mapper.LogRecordMapper"></mapper>
http://www.lryc.cn/news/2386324.html

相关文章:

  • AI|Java开发 IntelliJ IDEA中接入本地部署的deepseek方法
  • 【疑难杂症】Vue前端下载文件无法打开 已解决
  • 【1——Android端添加隐私协议(unity)1/3】
  • Linux之概述和安装vm虚拟机
  • 深入理解 Linux 的 set、env 和 printenv 命令
  • LeetCode热题100--19.删除链表的倒数第N个结点--中等
  • 开发AR导航助手:ARKit+Unity+Mapbox全流程实战教程
  • git学习与使用(远程仓库、分支、工作流)
  • 嵌入式预处理链接脚本lds和map文件
  • 9. Spring AI 各版本的详细功能与发布时间整理
  • 《Android 应用开发基础教程》——第十四章:Android 多线程编程与异步任务机制(Handler、AsyncTask、线程池等)
  • Apache 高级配置实战:从连接保持到日志分析的完整指南
  • 开源 OIDC(OpenID Connect)身份提供方(IdP)、iam选型
  • Android OkHttp控制链:深入理解网络请求的流程管理
  • 【JVM 01-引言入门篇】
  • Pandas数据规整
  • ThreadLocal线程本地变量在dubbo服务使用时候遇到的一个坑
  • pga 作用
  • setup.py Pip wheel
  • GO 语言进阶之 时间处理和Json 处理
  • 对WireShark 中的UDP抓包数据进行解析
  • Flannel后端为UDP模式下,分析数据包的发送方式(二)
  • 从 0 到 1:Spring Boot 与 Spring AI 深度实战(基于深度求索 DeepSeek)
  • upload-labs通关笔记-第20关 文件上传之杠点绕过
  • Vscode +Keil Assistant编译报错处理
  • 记录python在excel中添加一列新的列
  • WebRTC:实时通信的未来之路
  • 探索产品经理的MVP:从概念到实践
  • 用python实现中国象棋
  • GO 语言基础3 struct 结构体