news 2026/4/18 3:41:40

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 { // 拿到请求对象Request ServletRequestAttributes 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; /** * 自增主键 */ @TableId private 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 { @Autowired private 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: 初次提交

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/3/28 2:56:21

MusicFree插件完全指南:免费解锁全网音乐资源

MusicFree插件完全指南&#xff1a;免费解锁全网音乐资源 【免费下载链接】MusicFreePlugins MusicFree播放插件 项目地址: https://gitcode.com/gh_mirrors/mu/MusicFreePlugins 想要在一个应用中享受全网免费音乐&#xff1f;MusicFree插件系统就是你的终极解决方案。…

作者头像 李华
网站建设 2026/4/16 14:11:43

QMCDecode:让QQ音乐加密文件重获新生的终极方案

QMCDecode&#xff1a;让QQ音乐加密文件重获新生的终极方案 【免费下载链接】QMCDecode QQ音乐QMC格式转换为普通格式(qmcflac转flac&#xff0c;qmc0,qmc3转mp3, mflac,mflac0等转flac)&#xff0c;仅支持macOS&#xff0c;可自动识别到QQ音乐下载目录&#xff0c;默认转换结果…

作者头像 李华
网站建设 2026/4/8 2:58:52

DamaiHelper自动化抢票技术指南:3步轻松搞定热门演出门票

DamaiHelper自动化抢票技术指南&#xff1a;3步轻松搞定热门演出门票 【免费下载链接】DamaiHelper 大麦网演唱会演出抢票脚本。 项目地址: https://gitcode.com/gh_mirrors/dama/DamaiHelper 在热门演唱会门票一票难求的当下&#xff0c;DamaiHelper作为基于Python和Se…

作者头像 李华
网站建设 2026/4/18 1:01:35

英雄联盟智能辅助工具实战指南:让你的游戏体验更上一层楼

英雄联盟智能辅助工具实战指南&#xff1a;让你的游戏体验更上一层楼 【免费下载链接】LeagueAkari ✨兴趣使然的&#xff0c;功能全面的英雄联盟工具集。支持战绩查询、自动秒选等功能。基于 LCU API。 项目地址: https://gitcode.com/gh_mirrors/le/LeagueAkari 作为一…

作者头像 李华
网站建设 2026/4/16 17:14:50

猫抓扩展终极教程:轻松掌握网页视频下载神器

猫抓扩展终极教程&#xff1a;轻松掌握网页视频下载神器 【免费下载链接】cat-catch 猫抓 chrome资源嗅探扩展 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch 还在为无法保存心仪视频而苦恼吗&#xff1f;猫抓扩展作为一款专业的资源嗅探工具&#xff0c…

作者头像 李华
网站建设 2026/4/16 18:03:09

联想拯救者工具箱终极指南:3步掌握硬件控制与性能优化

联想拯救者工具箱终极指南&#xff1a;3步掌握硬件控制与性能优化 【免费下载链接】LenovoLegionToolkit Lightweight Lenovo Vantage and Hotkeys replacement for Lenovo Legion laptops. 项目地址: https://gitcode.com/gh_mirrors/le/LenovoLegionToolkit 联想拯救者…

作者头像 李华