本文是博主在做關于如何記錄用戶操作日志時做的記錄,常見的項目中難免存在一些需要記錄重要日志的部分,例如權限和角色設定,重要數據的操作等部分。
博主使用 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>