springboot实现用户操作日志记录

springboot实现用户操作日志记录

简介:之前写了《aop实现日志持久化记录》一文,主要介绍自定义aop标注方法上,通过切面方法对用户操作插入mysql。思路正确但是实际操作上存在一些小问题,本文将从项目出发,对细节进行补充。

另外值得一提的是,因为是基于AOP对controller方法做环绕通知实现的日志持久化记录,所以如果请求在Filter或者Interceptor中被拦截,则不会进入环绕通知,也就无法记录日志

1. 创建日志数据库表

数据库表结构大致如下

image-20231231171416729.png

建表语句(基于MySQL 5.7)

CREATE TABLE `admin_log` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '日志id',
  `ip` VARCHAR(20) DEFAULT NULL COMMENT '操作ip',
  `uri` VARCHAR(100) DEFAULT NULL COMMENT '请求URI',
  `method_type` VARCHAR(10) DEFAULT NULL COMMENT '请求类型(GET,POST)',
  `method_name` VARCHAR(100) DEFAULT NULL COMMENT '目标方法名',
  `method_desc` VARCHAR(20) DEFAULT NULL COMMENT '接口介绍',
  `request_param` TEXT COMMENT '请求参数',
  `status` VARCHAR(20) DEFAULT NULL COMMENT '请求状态',
  `result` TEXT COMMENT '返回结果',
  `user_id` VARCHAR(20) DEFAULT NULL COMMENT '操作者id',
  `execution_time` BIGINT(20) DEFAULT NULL COMMENT '方法耗时(ms)',
  `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
  PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4

2. 引入maven依赖

		<!-- 其他依赖在此不列出,springboot,mybatis等等	……  -->

        <!-- aop依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>

3. 创建数据库表对应实体类

@Data
public class AdminLog {

    private Integer id;
    private String userId;          // 用户id
    private String ip;              // 操作者ip
    private String uri;             // 请求URI
    private String methodType;      // 请求类型【GET,POST】
    private String methodName;      // 方法名称
    private String methodDesc;      // 接口简介
    private String requestParam;    // post请求参数
    private String status;          // 方法执行最终状态
    private String result;          // 返回结果
    private Long executionTime;     // 方法耗时
    private String createTime;      // 执行时间
    
}

4. 创建自定义注解

自定义注解,标注在要保存用户操作日志的controller方法上,被标注的方法会通过下面写的环绕通知进行日志记录

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface WriteLog {

    /**
     * 方法描述(描述目标方法的作用)
     */
    String value();

}

5. 创建上下文对象

从上面数据库表可以看出,日志类需要的ip、uri、methodType等参数需要在请求的request参数中获取,为记录这些参数信息,通过ThreadLocal设置上下文对象,方便获取。

先创建需要的请求信息的实体类

@Data
public class RequestBaseInfo {

    private String ip;
    private String methodType;  // 请求类型,如【GET,POST,PUT,DELETE】
    private String uri;
    
    public RequestBaseInfo(){}
    public RequestBaseInfo(String ip, String methodType, String uri){
        this.ip = ip;
        this.methodType = methodType;
        this.uri = uri;
    }

}

然后创建上下文对象,保存该类的对象

public class RequestContextHolder {

    private static final ThreadLocal<RequestBaseInfo> ipThreadLocal = new ThreadLocal<>();

    public static void setRequestBaseInfo(RequestBaseInfo requestBaseInfo) {
        ipThreadLocal.set(requestBaseInfo);
    }

    public static RequestBaseInfo getRequestBaseInfo() {
        return ipThreadLocal.get();
    }

    public static void clear() {
        ipThreadLocal.remove();
    }

}

6. 创建拦截器

创建拦截器,为每个线程保存上下文对象

/**
 * 该拦截器主要为线程保存请求的基本信息,例如来源ip,请求uri,请求方法等
 */
@Slf4j
@Component
public class SaveRequestBaseInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 将请求基本信息存到ThreadLocal,[ip,method,uri]
        RequestBaseInfo rbi = new RequestBaseInfo(request.getRemoteAddr(),request.getMethod(),request.getRequestURI());
        RequestContextHolder.setRequestBaseInfo(rbi);
        // 另外自己的框架里是否可以获取用户已登录的ip信息,没有的话这里还可以多设置一个获取登录用户的id
        // 因为我们的数据库日志表中有userId字段,如果没办法在后续的aop切面方法中获取,亦可以在这里拦截器中获取
        // 例如我项目中的spring security可以在aop切面中获取登录主体,拦截器就不需要获取了
        // 具体思路就是设置多一个上下文对象或者在RequestBaseInfo中设置多一个userId字段
        // 然后在这里获取请求token,然后在缓存中获取登录信息,获取登录者id
        // 当然实现的方式有多种,根据实际项目配置,或者不记录userId也可以
        // ……todo
        return true;
    }
}

将拦截器注册生效(配置进WebMvcConfigurer)

@Configuration
public class WebConfig  implements WebMvcConfigurer {

    @Autowired
    SaveRequestBaseInterceptor saveRequestBaseInterceptor;

    /**
     * 添加自定义拦截器
     * @param registry
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(saveRequestBaseInterceptor).addPathPatterns("/**");
    }

}

7. 创建AOP切面方法

在创建切面方法前还需要创建AdminLog表dao操作的相关代码,例如具体插入的service文件,mapper文件,这里就省略不说了,很基础的东西

aop实现针对以上自定义注解@WriteLog标注切面的环绕通知

@Aspect
@Component
public class WriteLogAspect {

    @Autowired
    AdminLogService adminLogService;

    // com.jankin.inoteadmin.system.annotation.WriteLog 是我定义接口的文件路径
    @Pointcut("@annotation(com.jankin.inoteadmin.system.annotation.WriteLog)")
    public void writeLogAspect() {}

    /**
     * 返回通知切面方法
     * @param joinPoint 切点,就是被注解的目标方法
     */
    @Around("writeLogAspect()")
    public Object logPostMapping(ProceedingJoinPoint joinPoint) throws Throwable {
        String userId = null; // 获取操作用户Id
    //  //我这里用的是SpringSecurity框架,这样获取UserId
    //	Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    //	if (authentication!=null){
    //  	// SecurityUser是自定义的UserDetails类,其中包含了UserId
    //    	SecurityUser principal = (SecurityUser)authentication.getPrincipal();
    //    	userId = principal.getUserId();
    //	}
        String status = "ERROR";
        String resultStr = "";
        Object result = null;
        long startTime = System.currentTimeMillis();    // 执行前时间
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            resultStr = e.getMessage();
            throw e;
        }finally {
            long finishTime = System.currentTimeMillis();   // 目标方法执行后时间
            if (result instanceof Result) {
                resultStr = result.toString();   // 返回结果字符串
                status = ((Result) result).getCode()==200? "SUCCESS":"EXCEPTION";
            }
            // 其他to do ……
            AdminLog sysLog = new AdminLog();
            sysLog.setUserId(userId);
            RequestBaseInfo rbi = RequestContextHolder.getRequestBaseInfo();
            sysLog.setIp(rbi.getIp());
            sysLog.setUri(rbi.getUri());
            sysLog.setMethodType(rbi.getMethodType());
            sysLog.setMethodName(joinPoint.getSignature().toShortString());
            // 获取注解上的方法描述
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            WriteLog annotation = signature.getMethod().getAnnotation(WriteLog.class);
            sysLog.setMethodDesc(annotation.value());
            sysLog.setRequestParam(Arrays.toString(joinPoint.getArgs()));
            sysLog.setStatus(status);
            sysLog.setResult(resultStr);
            sysLog.setExecutionTime(finishTime-startTime);
            adminLogService.addLog(sysLog);
        }
        return result;
    }

}

8. 应用

在接口(controller方法)上标注自定义注解(@WriteLog),即可完成接口日志的插入

    @WriteLog("测试接口Get")
    @GetMapping
    public Result get(){
        return Result.success("测试成功");
    }

    @WriteLog("测试接口Post")
    @PostMapping("post")
    public Result post(TestDto testDto){
        return Result.success("测试成功");
    }

测试结果

image-20240101002407422.png

至此,全篇结束

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

1、安装与配置

1、安装与配置 Redis 是完全开源的&#xff0c;遵守 BSD 协议&#xff0c;一款NoSql数据库&#xff08;非关系型数据库&#xff09;&#xff0c;高性能的key-value 数据库。 有以下三个特点&#xff1a; 1、Redis支持数据的持久化&#xff0c;可以将内存中的数据保存在磁盘中…

【AIGC-图片生成视频系列-4】DreamTuner:单张图像足以进行主题驱动生成

目录 一. 项目概述 问题&#xff1a; 解决&#xff1a; 二. 方法详解 a) 整体结构 b) 自主题注意力 三. 文本控制的动漫角色驱动图像生成的结果 四. 文本控制的自然图像驱动图像生成的结果 五. 姿势控制角色驱动图像生成的结果 2023年的最后一天&#xff0c;发个文记录…

使用keepalived时虚拟IP漂移注意事项

什么是Keepalived服务 keepalived是一个开源的软件项目&#xff0c;用于实现高可用性&#xff08;HA&#xff09;的网络服务器负载均衡和故障转移。它允许将多台服务器组合在一起&#xff0c;形成一个虚拟服务器集群&#xff0c;实现负载均衡和故障转移。 keepalived的核心功…

Conv2Former:一种transformer风格的卷积特征提取方式

一、前言 昨天读到了一篇有意思的文章&#xff0c;文章提出通过利用卷积调制操作来简化self-attention。还证明了这种简单的方法可以更好地利用卷积层中嵌套的大核(≥7 7)。我们都知道ViTs推动了设计识别模型的发展&#xff0c;近几年使用的也相当的多&#xff0c;通常就是CN…

C/C++面向对象(OOP)编程-回调函数详解(回调函数、C/C++异步回调、函数指针)

本文主要介绍回调函数的使用&#xff0c;包括函数指针、异步回调编程、主要通过详细的例子来指导在异步编程和事件编程中如何使用回调函数来实现。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;C/C精进之路 &…

ROS学习记录:使用RViz观测激光雷达传感器数据

一、使用CtrlAltT打开终端 二、输入 cd ~/catkin_ws1/ 进入工作空间 三、输入source ./devel/setup.bash 四、输入&#xff1a; roslaunch wpr_simulation wpb_simple.launch 打开机器人仿真环境 五、这是机器人仿真环境&#xff0c;里面机器人和书柜 六、再开一个终端&#…

开始使用MEVN技术栈开发01 概述

开始使用MEVN技术栈开发01 概述 简介 Welcome to Beginning MEVN Stack! This book focuses on the key tasks and concepts to get you started to learn and build MEVN stack applications in a faster pace. 欢迎阅读《MEVN堆栈入门》&#xff01;本书重点介绍关键任务…

echarts 二分图布局_力向导图_关系图

Echarts 常用各类图表模板配置 注意&#xff1a; 这里主要就是基于各类图表&#xff0c;更多的使用 Echarts 的各类配置项&#xff1b; 以下代码都可以复制到 Echarts 官网&#xff0c;直接预览&#xff1b; 图标模板目录 Echarts 常用各类图表模板配置一、力向导图(二分图布局…

【力扣题解】P98-验证二叉搜索树-Java题解

&#x1f468;‍&#x1f4bb;博客主页&#xff1a;花无缺 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! 本文由 花无缺 原创 收录于专栏 【力扣题解】 文章目录 【力扣题解】P98-验证二叉搜索树-Java题解&#x1f30f;题目描述&#x1f4a1;题解&#x1f30f;总…

分割数组的最大差值 - 华为OD统一考试

分割数组的最大差值 - 华为OD统一考试 OD统一考试 分值&#xff1a; 100分 题解&#xff1a; Java / Python / C 题目描述 给定一个由若干整数组成的数组nums &#xff0c;可以在数组内的任意位置进行分割&#xff0c;将该数组分割成两个非空子数组(即左数组和右数组)&#xf…

【2024最新版】neo4j安装配置

neo4j安装 写在最前面下载配置环境&#xff08;还是不行&#xff1f;&#xff09;启动neo4jpython中调用 写在最前面 之前我安装过&#xff0c;还写了一篇笔记 结果意外发现没有了&#xff0c;而且和之前安装的步骤不一样了&#xff0c;因此再次记录安装过程 下载 https://ne…

OpenGL FXAA抗锯齿算法(Qt)

文章目录 一、简介二、实现代码三、实现效果参考资料一、简介 之前已经提供了使用VCG读取Mesh的方式,接下来就需要针对读取的网格数据进行一些渲染操作了。在绘制Mesh数据时总会遇到图形的抗锯齿问题,OpenGL本身已经为我们提供了一种MSAA技术,但该技术对于一些实时渲染性能有…

【数据结构】栈和队列(栈的基本操作和基础知识)

&#x1f308;个人主页&#xff1a;秦jh__https://blog.csdn.net/qinjh_?spm1010.2135.3001.5343&#x1f525; 系列专栏&#xff1a;《数据结构》https://blog.csdn.net/qinjh_/category_12536791.html?spm1001.2014.3001.5482 目录 前言 栈 栈的概念和结构 栈的实现 ​…

Rust使用gRPC

需要先安装protoc&#xff08;Protocol Buffers Compiler&#xff09;&#xff0c;可据此Protobuf Compiler Installation下载 第一步&#xff1a;创建项目 创建两个新的Rust项目&#xff0c;分别作为服务端与客户端&#xff1a; cargo new rust_grpc_servercargo new rust_grp…

elasticsearch系列九:异地容灾-CCR跨集群复制

概述 起初只在部分业务中采用es存储数据&#xff0c;在主中心搭建了个集群&#xff0c;随着es在我们系统中的地位越来越重要&#xff0c;数据也越来越多&#xff0c;针对它的安全性问题也越发重要&#xff0c;那如何对es做异地容灾呢&#xff1f; 今天咱们就一起看下官方提供的…

Redis(上)

1、redis Redis是一个完全开源免费的高性能&#xff08;NOSQL&#xff09;的key-value数据库。它遵守BSD协议&#xff0c;使用ANSI C语言编写&#xff0c;并支持网络和持久化。Redis拥有极高的性能&#xff0c;每秒可以进行11万次的读取操作和8.1万次的写入操作。它支持丰富的数…

步进电机为什么叫步进电机,内部结构是什么,工作原理是什么,有什么特点,什么用途。

问题描述&#xff1a;步进电机为什么叫步进电机&#xff0c;内部结构是什么&#xff0c;工作原理是什么&#xff0c;有什么特点&#xff0c;什么用途。 问题解答&#xff1a; "步进"一词表示电机按照固定的步进角度运动。步进电机以控制脉冲信号来驱动转子按照一定的…

Vue2中使用echarts,并从后端获取数据同步

一、安装echarts npm install echarts -S 二、导入echarts 在script中导入&#xff0c;比如&#xff1a; import * as echarts from "echarts"; 三、查找要用的示例 比如柱状图 四、初始化并挂载 <template><div id"total-orders-chart" s…

三天吃透Java基础面试八股文

给大家分享我整理的Java高频面试题&#xff0c;有小伙伴靠他拿到字节offer了。 Java基础面试题 Java的特点Java 与 C 的区别JDK/JRE/JVM三者的关系Java程序是编译执行还是解释执行&#xff1f;面向对象和面向过程的区别&#xff1f;面向对象有哪些特性&#xff1f;数组到底是…

适用于电脑的 8 款文件/软件迁移软件 – 快速安全地更换电脑!

将文件/软件从一台设备传输到另一台设备已成为我们日常生活的重要组成部分&#xff0c;无论是出于个人目的还是出于职业目的。在当今快节奏的世界中&#xff0c;我们经常需要在不同设备之间传输大文件&#xff0c;例如视频、照片、文档等。虽然云服务提供了一种共享文件的好方法…