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:记录日志,对应的实体类,需要把记录的信息保存到数据库中;
- 用户实体类 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: 初次提交
如果此篇文章对你有帮助,感谢点个赞~~