当前位置: 首页 > article >正文

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:记录日志,对应的实体类,需要把记录的信息保存到数据库中;

        4) 用户实体类 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: 初次提交

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


http://www.kler.cn/a/372223.html

相关文章:

  • 【马来西亚理工大学主办,ACM出版】2025年大数据、通信技术与计算机应用国际学术会议(BDCTA 2025)
  • CTF知识点总结(二)
  • 卷积神经网络 (CNN, Convolutional Neural Network) 算法详解与PyTorch实现
  • Meilisearch ASP.Net Core API 功能demo
  • LeetCode 3019.按键变更的次数:遍历(转小写)
  • 【AJAX详解】
  • 电子电气架构 --- 汽车以太网概述
  • 论文翻译:ICLR 2024.DETECTING PRETRAINING DATA FROM LARGE LANGUAGE MODELS
  • 免费PDF页面提取小工具
  • 《自动驾驶技术的深度思考:安全与伦理的挑战》
  • ubuntu22安装搜狗输入法不能输入中文
  • Java 代码优化 修饰器模式(Decorator Pattern)
  • 搭建 mongodb 副本集,很详细
  • 【MySQL】C语言连接MySQL数据库3——事务操作和错误处理API
  • 电子电气架构 --- 电气系统工程
  • 【MySQL】 运维篇—备份与恢复:备份策略与方法
  • 商场应急响应:SpringBoot技术优化
  • 排序算法在最坏情况下的性能差异:深入分析
  • 音视频如何轻松转换?来看看这四款工具:
  • Android Html.fromHtml和buildSpannedString用途和实现方式
  • 探索C嘎嘎:初步接触STL
  • 【SQL】SQL函数
  • 鸿蒙生态崛起:开发者的机遇与挑战
  • 多IP访问网站
  • openjdk17 C++源码是怎么给java字段赋值的
  • 每天10个vue面试题(四)