news 2026/4/16 12:01:56

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/4/15 9:00:59

开源翻译模型进阶:HY-MT1.5插件生态

开源翻译模型进阶&#xff1a;HY-MT1.5插件生态 1. 引言&#xff1a;从通用翻译到场景化智能的跃迁 随着全球化进程加速&#xff0c;跨语言沟通需求呈现爆发式增长。传统机器翻译系统在面对复杂语境、混合语言和专业术语时往往力不从心。腾讯近期开源的混元翻译大模型 HY-MT1…

作者头像 李华
网站建设 2026/4/15 4:48:37

为什么你的游戏翻译总是不准确?这些真实经验分享或许能帮到你

为什么你的游戏翻译总是不准确&#xff1f;这些真实经验分享或许能帮到你 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 还记得那个令人沮丧的时刻吗&#xff1f;你兴奋地打开期待已久的海外游戏&#x…

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

开源年会抽奖系统快速部署与实战应用指南

开源年会抽奖系统快速部署与实战应用指南 【免费下载链接】lucky-draw 年会抽奖程序 项目地址: https://gitcode.com/gh_mirrors/lu/lucky-draw 你是否正在为年会抽奖环节发愁&#xff1f;传统抽奖方式效率低下&#xff0c;操作复杂&#xff0c;而且难以保证公平性。开源…

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

5步精通Unity游戏插件框架:BepInEx实战配置全解析

5步精通Unity游戏插件框架&#xff1a;BepInEx实战配置全解析 【免费下载链接】BepInEx Unity / XNA game patcher and plugin framework 项目地址: https://gitcode.com/GitHub_Trending/be/BepInEx Unity游戏插件框架BepInEx为游戏模组开发者提供了完整的解决方案&…

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

Blender 3MF插件完全攻略:3D打印工作流的高效解决方案

Blender 3MF插件完全攻略&#xff1a;3D打印工作流的高效解决方案 【免费下载链接】Blender3mfFormat Blender add-on to import/export 3MF files 项目地址: https://gitcode.com/gh_mirrors/bl/Blender3mfFormat Blender 3MF插件是专门为3D打印行业设计的开源扩展工具…

作者头像 李华
网站建设 2026/4/16 10:40:44

纪念币预约神器:5分钟搞定抢购难题,成功率提升300%

纪念币预约神器&#xff1a;5分钟搞定抢购难题&#xff0c;成功率提升300% 【免费下载链接】auto_commemorative_coin_booking 项目地址: https://gitcode.com/gh_mirrors/au/auto_commemorative_coin_booking 还在为纪念币预约手忙脚乱而烦恼吗&#xff1f;每次预约时…

作者头像 李华