SpringBoot最佳实践之 - 使用AOP记录操作日志

1. 前言

本篇博客是个人在工作中遇到的需求。针对此需求,开发了具体的实现代码。并不是普适的记录操作日志的方式。以阅读本篇博客的朋友,可以参考此篇博客中记录日志的方式,可能会对你有些许帮助和启发。

2. 需求描述

有一个后台管理系统,此系统具有不同角色的用户,比如管理员、操作员、审计员等。当这些角色的用户登录到系统中,以及其在系统中所触发的 <增删改> 操作。我都想记录操作日志。然后存储到数据库中。比如记录如下:

数据库中有了数据,就可以在查询出来显示到页面上。对于一个业务敏感的后台管理系统来说,就可以通过这里查看哪些用户操作了什么功能。操作的结果是成功还是失败,如果操作失败,失败的原因是什么。如下:

3. 需求实现

3.1 准备工作

3.1.1 导入依赖
     <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></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.5.3.2</version></dependency>
3.1.2 数据库脚本

用户表 t_user

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for t_user
-- ----------------------------
DROP TABLE IF EXISTS `t_user`;
CREATE TABLE `t_user`  (`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键',`user_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '用户名',`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',`phone` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '手机号',`email` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '邮箱',`create_time` datetime(0) NULL DEFAULT NULL COMMENT '创建时间',`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',`status` tinyint(4) NULL DEFAULT 0 COMMENT '用户状态(0:可用;1:禁用)',`delete_flag` tinyint(4) NULL DEFAULT NULL COMMENT '删除标记(0:未删除;1:已删除)',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of t_user
-- ----------------------------
INSERT INTO `t_user` VALUES (1, '张三', '123456', '18178526349', '123@qq.com', '2024-10-29 08:42:34', '2024-10-29 08:42:37', 0, 0);SET FOREIGN_KEY_CHECKS = 1;

操作日志表 t_system_log

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
-- Table structure for t_system_log
-- ----------------------------
DROP TABLE IF EXISTS `t_system_log`;
CREATE TABLE `t_system_log`  (`id` int(11) NOT NULL AUTO_INCREMENT,`operate_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '触发的动作',`operate_user_name` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作用户名',`operate_time` varchar(40) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '操作时间',`operate_result` tinyint(4) NULL DEFAULT NULL COMMENT '0成功/1失败',`operate_fail_reason` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '操作失败原因',PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 800 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;-- ----------------------------
-- Records of t_system_log
-- ----------------------------
INSERT INTO `t_system_log` VALUES (792, '登录', '张三', '2024-10-29 10:06:09', 0, NULL);
INSERT INTO `t_system_log` VALUES (793, '登录', '张三', '2024-10-29 10:07:13', 1, '用户名或密码错误');
INSERT INTO `t_system_log` VALUES (794, '登录', '张三', '2024-10-29 10:09:22', 1, '用户名或密码错误');
INSERT INTO `t_system_log` VALUES (795, '登录', '张三', '2024-10-29 10:11:31', 1, '用户名或密码错误');
INSERT INTO `t_system_log` VALUES (796, '添加商品', '张三', '2024-10-29 10:19:11', 0, NULL);
INSERT INTO `t_system_log` VALUES (797, '添加商品', '张三', '2024-10-29 10:19:32', 1, '商品已存在');
INSERT INTO `t_system_log` VALUES (798, '下架商品', '张三', '2024-10-29 10:41:58', 0, NULL);
INSERT INTO `t_system_log` VALUES (799, '下架商品', '张三', '2024-10-29 10:42:22', 1, '商品正在发货中,无法下架');SET FOREIGN_KEY_CHECKS = 1;

3.2 需要的组件说明

1)自定义注解 @Operation:把自定义注解标注在Controller方法上,后续通过切面识别Controller方法上标注的注解,以及注解的value值,从而实现记录操作日志功能;

2)切面类 LogAspect: 识别标注有@Operation注解的Controller方法,在方法执行过程中进行切面操作;

3)日志实体类 SystemLog:记录日志,对应的实体类,需要把记录的信息保存到数据库中;

  1. 用户实体类 User: 用户实体类;

5)业务异常类:自定义的异常类;

6)统一错误码枚举类:自定义的错误码枚举类,把项目中出现的错误码统一存放在此处,便于管理;

3.3 组件代码

3.3.1 自定义注解 @Operation
package com.shg.annotation;import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Operation {String value();
}
3.3.2 切面类 LogAspect
package com.shg.aspect;import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.shg.annotation.Operation;
import com.shg.model.pojo.SystemLog;
import com.shg.model.pojo.User;
import com.shg.service.RecordLogService;
import com.shg.service.UserService;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Objects;@Component
@Aspect
public class LogAspect {private final UserService userService;private final RecordLogService recordLogService;public LogAspect(UserService userService, RecordLogService recordLogService) {this.userService = userService;this.recordLogService = recordLogService;}@Pointcut(value = "@annotation(com.shg.annotation.Operation)")private void pointCut() {}@Around(value = "pointCut()")public Object recordLog(ProceedingJoinPoint pjp) throws Throwable {// 拿到请求对象RequestServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = requestAttributes.getRequest();// 通过request获取请求头中的登录用户[此处是模拟直接在请求头中携带一个用户id,真实开发是在请求头中携带一个token,然后通过token去redis中查询用户信息,包括用户权限信息等]String userId = request.getHeader("userId");// 通过userId 去数据库中查询用户信息User userFromDB = userService.getById(userId);// 拿到方法上标注的自定义注解的value值,这样就可以知道当前这个用户是在做什么操作了MethodSignature methodSignature = (MethodSignature) pjp.getSignature();Method method = methodSignature.getMethod();Operation annotation = method.getAnnotation(Operation.class);String value;Object result = null;if (!Objects.isNull(annotation)) {value = annotation.value();// 当你在某个方法上标注了 @Operation自定义注解,并且给这个注解的value进行合法赋值后,才记录日志(比如增删改操作),而对于查询方法,一般不需要在Controller方法上标注@Operation注解if (StrUtil.isNotBlank(value)) {SystemLog systemLog = new SystemLog();systemLog.setOperateName(value);systemLog.setOperateUserName(userFromDB.getUserName());systemLog.setOperateTime(DateUtil.formatDateTime(new Date()));try {result = pjp.proceed();systemLog.setOperateResult(0);recordLogService.save(systemLog);} catch (Exception e) {systemLog.setOperateResult(1);systemLog.setOperateFailReason(e.getMessage());recordLogService.save(systemLog);throw e;} finally {System.out.println("finally...");}}}return result;}
}
3.3.3 日志实体类
package com.shg.model.pojo;import java.io.Serializable;import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;@Data
@TableName("t_system_log")
public class SystemLog implements Serializable {private Integer id;private String operateName;private String operateUserName;private String operateTime;private String operateFailReason;/*** 0成功/1失败*/@ApiModelProperty("0成功/1失败")private Integer operateResult;}
3.3.4 用户实体类
package com.shg.model.pojo;import java.time.LocalDateTime;
import java.util.Date;import java.io.Serializable;import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("t_user")
public class User implements Serializable {private static final long serialVersionUID = -45223488720491550L;/*** 自增主键*/@TableIdprivate Integer id;/*** 用户名*/private String userName;/*** 密码*/private String password;/*** 手机号*/private String phone;/*** 邮箱*/private String email;/*** 创建时间*/private LocalDateTime createTime;/*** 修改时间*/private LocalDateTime updateTime;/*** 用户状态(0:可用;1:禁用)*/private Integer status;/*** 删除标记(0:未删除;1:已删除)*/private Integer deleteFlag;}
3.3.5 业务异常类
package com.shg.exception;import com.shg.common.ResponseCodeEnum;
import lombok.Data;@Data
public class BizException extends RuntimeException{private Integer code;private String message;public BizException(Integer code, String message) {super(message);this.code = code;this.message = message;}public BizException(ResponseCodeEnum responseCodeEnum) {super(responseCodeEnum.getMessage());this.code = responseCodeEnum.getCode();this.message = responseCodeEnum.getMessage();}}
3.3.6统一错误码枚举类
package com.shg.common;public enum ResponseCodeEnum {SUCCESS(0, "success"),SYSTEM_EXCEPTION(500, "System internal exception"),USERNAME_OR_PASSWORD_FAIL(1001, "用户名或密码错误"),USER_NOT_EXISTS(1002,"用户不存在"), GOODS_ID_EXISTS(2001, "商品已存在"),DELETE_GOODS_FAIL(2001, "商品正在发货中,无法下架");private final int code;private final String message;ResponseCodeEnum(int code, String message) {this.code = code;this.message = message;}public int getCode() {return code;}public String getMessage() {return message;}}
3.2.7 Controller类
package com.shg.controller;import com.shg.annotation.Operation;
import com.shg.common.ResponseCodeEnum;
import com.shg.common.ResultMessage;
import com.shg.exception.BizException;
import com.shg.model.pojo.User;
import com.shg.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;@RestController
public class TestController {@Autowiredprivate UserService userService;@GetMapping("/test1")public ResultMessage<String> test1() {return ResultMessage.success("这是测试接口...");}@Operation(value = "登录")@GetMapping(value = "/login")public ResultMessage login(Integer id) {User user = userService.getById(1);if (user.getId() == 1) {throw new BizException(ResponseCodeEnum.USERNAME_OR_PASSWORD_FAIL);}return ResultMessage.success("登录成功", user);}@Operation(value = "添加商品")@PostMapping(value = "/addGoods")public ResultMessage addGoods(@RequestParam Integer goodsId) {if (goodsId == 2) {throw new BizException(ResponseCodeEnum.GOODS_ID_EXISTS);}return ResultMessage.success("商品添加成功", "模拟添加商品成功");}@Operation(value = "下架商品")@PostMapping(value = "/deleteGoods")public ResultMessage deleteGoods(@RequestParam Integer goodsId) {if (goodsId == 4) {throw new BizException(ResponseCodeEnum.DELETE_GOODS_FAIL);}return ResultMessage.success("商品下架成功", "模拟商品下架成功");}
}

5. 其他

具体代码示例参考:springboot-best-practice: 初次提交

如果此篇文章对你有帮助,感谢点个赞~~

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/37810.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

投影算子(Projection Operator)的定义、性质、分类以及应用

文章目录 1. 投影算子的定义2. 投影算子的几何意义3. 一些简单的例子例 1&#xff1a;二维平面上的投影例 2&#xff1a;投影到一条任意方向的直线例 3&#xff1a;三维空间中投影到一个平面 4. 投影算子的性质4.1、幂等性&#xff08;Idempotency&#xff09;&#xff1a; P 2…

VLAN综合实验报告

一、实验拓扑 网络拓扑结构包括三台交换机&#xff08;LSW1、LSW2、LSW3&#xff09;、一台路由器&#xff08;AR1&#xff09;以及六台PC&#xff08;PC1-PC6&#xff09;。交换机之间通过Trunk链路相连&#xff0c;交换机与PC、路由器通过Access或Hybrid链路连接。 二、实验…

coding ability 展开第五幕(二分查找算法)超详细!!!!

. . 文章目录 前言二分查找搜索插入的位置思路 x的平方根思路 山脉数组的峰顶索引思路 寻找旋转排序数组中的最小值思路 总结 前言 本专栏上篇博客已经把滑动指针收尾啦 现在还是想到核心——一段连续的区间&#xff0c;有时候加上哈希表用起来很爽 今天我们来学习新的算法知识…

文献阅读篇#2:YOLO改进类的文章如何高效进行文献阅读(对于初学者)

对于初学者来说&#xff0c;文献阅读是非常非常重要的一个学习方式&#xff0c;好的文献阅读方法会让学习的效率翻倍。我希望能够总结出一套比较有效的文献阅读方法&#xff0c;并通过记录的方法来找到不足和可改进之处 一、文献检索 对于初学者来说&#xff0c;应当先从中文…

数智读书笔记系列021《大数据医疗》:探索医疗行业的智能变革

一、书籍介绍 《大数据医疗》由徐曼、沈江、余海燕合著&#xff0c;由机械工业出版社出版 。徐曼是南开大学商学院副教授&#xff0c;在大数据驱动的智能决策研究领域颇有建树&#xff0c;尤其在大数据驱动的医疗与健康决策方面有着深入研究&#xff0c;曾获天津优秀博士论文、…

MarsCode AI实战:利用DeepSeek 快速搭建你的口语学习搭子

资料来源&#xff1a;火山引擎-开发者社区 成品抢先看&#xff01; 自从MarsCode AI Chat模型全新升级&#xff0c;接入 Deepseek-R1、Deepseek-V3和豆包大模型1.5 三大模型&#xff0c;越来越多朋友注意到了AI编程能给我们带来的无限可能&#xff0c;也开始跃跃欲试想要尝试从…

Linux环境变量:深入解析与实用指南

目录 一、环境变量概述 二、环境变量的作用 三、环境变量的类型 3.1系统环境变量 3.2用户环境变量 四、环境变量的操作 4.1查看环境变量 4.2设置环境变量 4.3删除环境变量 五、环境变量的配置文件 六、环境变量的最佳实践 七、总结 环境变量是Linux系统中至关重要的…

C++20 线程协调类:从入门到精通

文章目录 1. 初识线程协调2. std::barrier&#xff1a;多线程同步的屏障2.1 核心函数2.2 示例代码2.3 高级用法2.4 适用场景 3. std::latch&#xff1a;一次性同步原语3.1 核心函数3.2 示例代码3.3 高级用法3.4 适用场景 4. std::counting_semaphore&#xff1a;可重用的同步原…

【Linux网络】手动部署并测试内网穿透

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;博客仓库&#xff1a;https://gitee.com/JohnKingW/linux_test/tree/master/lesson &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &…

MySQL中的锁机制:从全局锁到行级锁

目录 1. 锁的基本概念 2. 全局锁 2.1 全局锁的定义 2.2 全局锁的类型 2.3 全局锁的使用场景 2.4 全局锁的实现方式 2.5 全局锁的优缺点 2.6 全局锁的优化 3. 表级锁 3.1 表级锁的类型 3.2 表级锁的使用场景 3.3 表级锁的优缺点 4. 意向锁&#xff08;Intention Lo…

2025年渗透测试面试题总结- 某亭-安全研究员(题目+回答)

网络安全领域各种资源&#xff0c;学习文档&#xff0c;以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具&#xff0c;欢迎关注。 目录 一、SQL注入过滤单引号绕过方法 二、MySQL报错注入常用函数 三、报错注入绕WAF 四、MySQL写文件函数…

MacOS安装 nextcloud 的 Virtual File System

需求 在Mac上安装next cloud实现类似 OneDrive 那样&#xff0c;文件直接保存在服务器&#xff0c;需要再下载到本地。 方法 在 官网下载Download for desktop&#xff0c;注意要下对版本&#xff0c;千万别下 Mac OS默认的那个。 安装了登录在配置过程中千万不要设置任何同…

1.8 函数的连续性和间断点

1.连续的定义 2.间断点的定义 3.间断点的分类

Unity 云渲染本地部署方案

Unity Render Streaming 云渲染环境搭建 0.安装 Unity Render Streaming 实现原理: 服务器与客户端实现功能包括: 详细内容见官方文档&#xff1a; 官方文档: https://docs.unity3d.com/Packages/com.unity.renderstreaming3.1/manual/tutorial.html Unity 流送云渲染介绍: …

每日一题力扣3248.矩阵中的蛇c++

3248. 矩阵中的蛇 - 力扣&#xff08;LeetCode&#xff09; class Solution { public:int finalPositionOfSnake(int n, vector<string>& commands) {int i 0;int j 0;for (int k0;k<commands.size();k) {if (commands[k] "RIGHT")j;else if (comma…

本地基于Ollama部署的DeepSeek详细接口文档说明

前文&#xff0c;我们已经在本地基于Ollama部署好了DeepSeek大模型&#xff0c;并且已经告知过如何查看本地的API。为了避免网络安全问题&#xff0c;我们希望已经在本地调优的模型&#xff0c;能够嵌入到在本地的其他应用程序中&#xff0c;发挥本地DeepSeek的作用。因此需要知…

FPGA 以太网通信(三)

一、UDP协议 UDP&#xff08;User Datagram Protocol Protocol&#xff09;&#xff0c;即用户数据报协议&#xff0c;是一种面向无连接的传输层协议。UDP和TCP协议都属于传输层协议&#xff0c;在网络传输中同一 IP 服务器需要提供各种不同的服务&#xff0c;为了区别不同的服…

期刊分区表2025年名单下载(经济学、管理学)

2025年期刊分区表包括SCIE、SSCI、A&HCI、ESCI和OAJ&#xff0c;共设置了包括自然科学、社会科学和人文科学在内的21个大类 本次分享的是期刊分区表2025年名单经济学类、管理学类&#xff0c;一共7631025条 一、数据介绍 数据名称&#xff1a;期刊分区表2025年名单 数据…

如何在MCU工程中启用HardFault硬错误中断

文章目录 一、HardFault出现场景二、启动HardFault三、C代码示例 一、HardFault出现场景 HardFault&#xff08;硬故障&#xff09; 错误中断是 ARM Cortex-M 系列微控制器中一个较为严重的错误中断&#xff0c;一旦触发&#xff0c;表明系统遇到了无法由其他异常处理机制解决…

智能体开发革命:灵燕平台如何重塑企业AI应用生态

在AI技术深度渗透产业的今天&#xff0c;**灵燕智能体平台**以“全生命周期管理”为核心&#xff0c;为企业提供从智能体开发、协作到落地的闭环解决方案&#xff0c;开创了AI应用工业化生产的新模式。 三位一体的智能体开发体系 1. Agent Builder&#xff1a;零门槛构建专属…