本文是博主在做关于如何记录用户操作日志时做的记录,常见的项目中难免存在一些需要记录重要日志的部分,例如权限和角色设定,重要数据的操作等部分。
博主使用 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_type
on project_log (operate_type);
create index idx_operate_module
on project_log (operate_module);
create index idx_uid
on project_log (uid);
create index idx_company
on project_log (company);
create index idx_uname
on project_log (uname);
04、项目结构图
05、代码:application.yml
server:
port: 8089
spring:
datasource:
url: jdbc:mysql://10.100.4.163:3306/test?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 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 {
@Autowired
private IUserService userService;
@Autowired
private 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();
}
}
@Async
private 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 {
@Autowired
private 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 {
@Override
public 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>