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>