springboot实现用户操作日志记录

news2025/7/11 17:28:11

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

至此,全篇结束

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vue3+vant4 移动端软键盘弹出 收起导致项目样式布局错乱解决方案,亲测有效!!

问题描述 最近在做vue3 H5的移动端项目 我用的是vue3vant4&#xff0c;然后在使用过程中发现 小米14手机在点击密码输入框软键盘弹出 时会导致项目布局整体向上移动 导致页面布局错乱。 原因分析&#xff1a; 在移动端软键盘弹出收起时&#xff0c;导致项目样式布局错乱的原因…

内网DNS隐蔽隧道搭建之iodine工具

iodine iodine是基于C语言开发的&#xff0c;分为服务端和客户端。iodine支持转发模式和中继模式。其原理是&#xff1a;通过TAP虚拟网卡&#xff0c;在服务端建立一个局域网&#xff1b;在客户端&#xff0c;通过TAP建立一个虚拟网卡&#xff1b;两者通过DNS隧道连接&#xf…

前端开发加速器:十个VSCode插件精选

前端开发是一个不断发展的领域&#xff0c;随着技术的进步&#xff0c;工具也在不断更新。Visual Studio Code&#xff08;VSCode&#xff09;是前端开发者广泛使用的编辑器之一&#xff0c;得益于其强大的插件系统&#xff0c;可以帮助开发者提升工作效率。以下是十个对于前端…

任务需求分析中的流程图、用例图、er图、类图、时序图线段、图形的作用意义

任务需求分析中的流程图、用例图、er图、类图、时序图线段、图形的作用意义 流程图 流程图中各种图形的含义及用法解析 连接线符号 连接各要素&#xff0c;表示流程的顺序或过程的方向。 批注符号 批注或说明&#xff0c;也可以做条件叙述。 子流程 流程中一部分图形的逻辑…

程序员如何应对裁员-法律知识

目录 程序员如何应对裁员-法律知识 前言收到辞退消息后被要求主动离职可以赔偿多少呢裁员赔偿工资是指基本工资吗被强制接触劳动合同&#xff0c;也不愿意支付赔偿孕妇&#xff0c;公司让离职谈话之后&#xff0c;老板还是不愿意给补偿&#xff0c;我应该怎么办&#xff1f;离…

IO作业4.0

思维导图 创建出三个进程完成两个文件之间拷贝工作&#xff0c;子进程1拷贝前一半内容&#xff0c;子进程2拷贝后一半内容&#xff0c;父进程回收子进程的资源 #include <stdio.h> #include <string.h> #include <stdlib.h> #include <myhead.h> int …

CCNP课程实验-07-OSPF-Trouble-Shooting

目录 实验条件网络拓朴 环境配置开始排错错点1&#xff1a;R1-R2之间认证不匹配错误2&#xff1a;hello包的时间配置不匹配错误3&#xff1a;R2的e0/1接口区域配置不正确错误4&#xff1a;R4的e0/1接口没有配置进OSPF错误5&#xff1a;R2的区域1没有配置成特殊区域错误6&#x…

ASP.NETCore WebAPI 入门 杨中科

ASP.NETCore WebAPI入门1 回顾 mvc开发模式 前端代码和后端代码是混在一个项目之中 WEB API 1、什么是结构化的Http接口。Json。 2、Web API项目的搭建。 3、Web API项目没有Views文件夹。 4、运行项目&#xff0c;解读代码结构。 5、【启用OpenAPI支持】→>swagger,在界…

Pytorch从零开始实战15

Pytorch从零开始实战——ResNeXt-50算法实战 本系列来源于365天深度学习训练营 原作者K同学 文章目录 Pytorch从零开始实战——ResNeXt-50算法实战环境准备数据集模型选择开始训练可视化总结 环境准备 本文基于Jupyter notebook&#xff0c;使用Python3.8&#xff0c;Pytor…

高效分割视频:批量剪辑,轻松提取m3u8视频技巧

在数字媒体时代&#xff0c;视频分割是一项常见的需求。无论是为了编辑、分享还是其他要求&#xff0c;经常要将长视频分割成多个短片。传统的视频分割方法往往需要手动操作&#xff0c;既耗时又容易出错。现在来看云炫AI智剪高效分割视频的方法&#xff0c;批量剪辑并轻松提取…

nest框架的bull队列的基本使用

前言 nestjs/bull是一个用于处理队列的Nest.js模块。它基于Bull库&#xff0c;提供了在Nest框架中使用队列的功能&#xff0c;这个模块要跟redis联合起来使用 1.下载队列模块&#xff1a; npm install --save nestjs/bull bull redis 项目结构&#xff1a; 2. 在模块中导入B…

佳能G3800彩色喷墨多功能一体打印机报5B00错误代码处理方法

5B00错误代码的含义 5B00错误代码是指佳能G3800打印机的“废墨仓已满”。这个废墨仓是打印机内部的一个部件&#xff0c;主要用于收集打印过程中产生的废墨。当废墨仓已满时&#xff0c;打印机就会报5B00错误代码。 佳能G3800彩色喷墨多功能一体打印机报5B00错误代码处理办法 …

vue3中标签form插件

想写一个系统&#xff0c;对八字进行标注&#xff0c;比如格局&#xff0c;有些八字就有很多格局&#xff0c;于是就想着使用el-tag但是&#xff0c;form表单中如何处理呢&#xff1f; 这个时候&#xff0c;就需要自己写一个,modelValue是表单的默认属性 <template><…

uniCloud 云数据库(新建表、增、删、改、查)

新建表结构描述文件 todo 为自定义的表名 表结构描述文件的默认后缀为 .schema.json 设置表的操作权限 uniCloud-aliyun/database/todo.schema.json 默认的操作权限都是 false "permission": {"read": false,"create": false,"update&quo…

stm32学习总结:5、Proteus8+STM32CubeMX+MDK仿真串口并使用串口打印日志(注意重定向printf到串口打印的问题)

stm32学习总结&#xff1a;5、Proteus8STM32CubeMXMDK仿真串口并使用串口打印日志&#xff08;注意重定向printf到串口打印的问题&#xff09; 文章目录 stm32学习总结&#xff1a;5、Proteus8STM32CubeMXMDK仿真串口并使用串口打印日志&#xff08;注意重定向printf到串口打印…

网工内推 | 事业单位、上市公司网工,五险一金补贴多

01 中科中山药物创新研究院 招聘岗位&#xff1a;网络工程师 职责描述&#xff1a; 1.负责信息化网络数据安全&#xff0c;加固网络安全、渗透测试、风险评估、漏洞扫描、风险管理和风险评估方法。对防火墙、IDS、IPS、蜜罐、防DDOS、VPN等设备的运行管理&#xff1b; 2.负责机…

FPGA——VIVADO生成固化文件,掉电不丢失

VIVADO生成固化文件 (1)加入代码(2)生成bin文件&#xff0c;并且下载 (1)加入代码 设计文件(.xdc)中加入这段代码: set_property CFGBVS VCCO [current_design] set_property CONFIG_VOLTAGE 3.3 [current_design] set_property BITSTREAM.GENERAL.COMPRESS true [current_de…

斯坦福和 Meta学者发现Gemini在常识推理任务中有较强潜力;初学者GPT:Ai和LLM资源

&#x1f989; AI新闻 &#x1f680; 斯坦福和 Meta学者发现Gemini在常识推理任务中有较强潜力 摘要&#xff1a;斯坦福和Meta的学者发表论文为Gemini正名&#xff0c;他们发现之前对Gemini的评估并不能完全捕捉到其真正的常识推理潜力。他们设计了需要跨模态整合常识知识的任…

net8 golang python性能比较

net8正式版出来两个月&#xff0c;现在性能到底如何呢&#xff0c;做个简单的例子和其他语言比较一下&#xff0c;测试内容是查找1000000以内的质数&#xff0c;代码不多&#xff0c;但包含了循环计算和Math库函数调用&#xff0c;直观的看一下语言之间差距是多少&#xff0c;心…

Strict MIME type checking is enforced for module scripts per HTML spec.

目录 前言错误信息如下:前言 最近使用docker打包Nginx和vue 为镜像文件,启动镜像时报错 错误信息如下: index89886.js:1 Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/html". Stri…