Java多线程实战-异步操作日志记录解决方案(AOP+注解+多线程)

🏷️个人主页:牵着猫散步的鼠鼠 

🏷️系列专栏:Java全栈-专栏

🏷️本系列源码仓库:多线程并发编程学习的多个代码片段(github)

🏷️个人学习笔记,若有缺误,欢迎评论区指正 

本章节案例源码:1321928757/Concurrent-MulThread-Demo: 多线程并发编程学习的多个代码片段,干货分享集合~~~ (github.com)

目录

前言

实现思路

自定义OperationLog注解

使用AOP切面拦截被注解标记的方法

获取请求信息构建OperationLogVo对象

编写线程池封装类,封装类工厂

使用线程池异步执行日志记录操作

使用日志注解,测试

异步日志记录的优缺点分析

优点

缺点

日志持久化方案分析

总结


前言

在现代分布式系统中,操作日志记录扮演着非常重要的角色。它不仅能够帮助我们追踪系统的运行状态,还可以提供关键的审计线索,对于系统的运维和问题排查都有着重要意义。传统的日志记录方式通常是在相关的业务逻辑代码中直接插入日志记录语句,这种方式虽然直观简单,但存在一些明显的缺陷:

  1. 日志记录代码和业务逻辑代码高度耦合,不利于代码的可维护性。
  2. 新增或修改日志记录需求时,需要修改多处代码,工作量较大。
  3. 由于日志记录操作通常需要进行IO操作,会对业务响应时间产生一定影响。

为了解决这些问题,我们可以考虑采用基于注解和AOP切面的异步日志记录解决方案。它能够有效地将日志记录代码和业务逻辑代码解耦,同时通过异步的方式避免日志记录阻塞主线程,从而提高系统的响应速度和吞吐量。

实现思路

自定义OperationLog注解

我们首先定义一个OperationLog注解,用于标记需要记录操作日志的方法。该注解可以包含一些属性,如操作描述、操作类型等,方便后续记录日志时获取相关信息。

@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OperationLog {/*** @return 操作描述*/String value() default "";
}

使用AOP切面拦截被注解标记的方法

接下来,我们需要定义一个AOP切面,通过切点表达式拦截被OperationLog注解标记的方法。在切面的增强方法中,我们可以获取方法的元数据信息、请求参数等,并与HTTP请求信息一起构建出OperationLogVo对象。

@Aspect
@Component
@Slf4j
public class OperationLogAspect {@Pointcut("@annotation(com.luckysj.demo.annotation.OperationLog)")public void optLogPointCut() {}@Around("optLogPointCut()")public Object around(ProceedingJoinPoint joinPoint) {// 环绕增强方法...}
}

AOP配合注解注解使用是一种很常见且使用的手段,像限流,鉴权之类与业务无关的操作,我们都可以通过这种方法来将这些辅助业务从主业务中拆开来,减少代码耦合度。

获取请求信息构建OperationLogVo对象

在切面的增强方法中,我们使用反射的方式获取目标方法的元数据信息,包括方法名、所在类名等。同时,我们还需要从当前线程绑定的RequestContextHolder中获取HttpServletRequest对象,以获取请求的URI、请求方法、IP地址等信息。将这些信息与操作描述等数据组合,即可构建出完整的OperationLogVo对象。

日志实体对象OperationLogVo:

@Data
@TableName("operation_log")
public class OperationLogVo {@TableId(type = IdType.AUTO)private Long logId;private String type;@TableField("request_uri")private String uri;private String name;@TableField("ip_address")private String ipAddress;private String method;private String params;private String data;@TableField("nick_name")private String nickname;private Integer userId;private Long times;private String errorMessage;}

在AOP切面类中定义一个从织入点中获取数据组装OperationLogVo 实体的方法:

    private OperationLogVo recordLog(ProceedingJoinPoint joinPoint) {// 从切面织入点处通过反射机制获取织入点处的方法MethodSignature signature = (MethodSignature) joinPoint.getSignature();// 获取切入点所在的方法Method method = signature.getMethod();// 获取操作OperationLog optLogger = method.getAnnotation(OperationLog.class);// 获取requestServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();// 日志保存到数据库OperationLogVo operationLogVo = new OperationLogVo();// 操作类型operationLogVo.setType(optLogger.value());// 请求URIoperationLogVo.setUri(request.getRequestURI());// 获取请求的类名String className = joinPoint.getTarget().getClass().getName();// 获取请求的方法名String methodName = method.getName();methodName = className + "." + methodName;// 请求方法operationLogVo.setName(methodName);// 请求参数if (joinPoint.getArgs()[0] instanceof MultipartFile) {operationLogVo.setParams(((MultipartFile) joinPoint.getArgs()[0]).getOriginalFilename());} else {operationLogVo.setParams(JSON.toJSONString(joinPoint.getArgs()));}// 请求方式operationLogVo.setMethod(Objects.requireNonNull(request).getMethod());// 请求用户ID 先写死operationLogVo.setUserId(22);
//        operationLogVo.setUserId(SecurityUtils.getUserId());// 请求用户昵称 先写死operationLogVo.setNickname("woniu");// 操作ip地址String ip = request.getRemoteAddr();operationLogVo.setIpAddress(ip);return operationLogVo;}

 我们这里还需要一个方法来处理异常信息,将异常信息格式化为字符串,方便存储

// 将异常相关的全部信息(类名、描述、堆栈跟踪)格式化为一个字符串,方便存储到日志记录对象OperationLogVo的errorMessage属性中。
public String stackTraceToString(String exceptionName, String exceptionMessage, StackTraceElement[] elements) {StringBuilder stringBuilder = new StringBuilder();for (StackTraceElement stet : elements) {stringBuilder.append(stet).append("\n");}return exceptionName + ":" + exceptionMessage + "\n" + stringBuilder;
}

编写线程池封装类,封装类工厂

AsyncManager类是一个单例类,内部维护了一个ScheduledExecutorService线程池executor。

我们封装了一些常用的方法:

public class AsyncManager {/*** 单例模式,确保类只有一个实例*/private AsyncManager() {}/*** 饿汉式,在类加载的时候立刻进行实例化*/private static final AsyncManager INSTANCE = new AsyncManager();public static AsyncManager getInstance() {return INSTANCE;}/*** 异步操作任务调度线程池*/private final ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");/*** 执行任务** @param task 任务*/public void execute(TimerTask task) {executor.schedule(task, 10, TimeUnit.MILLISECONDS);}/*** 停止任务线程池*/public void shutdown() {ThreadUtils.shutdownAndAwaitTermination(executor);}}

工厂类:

public class AsyncFactory {/*** 记录操作日志* @param operationLog 操作日志信息* @return 任务task*/public static TimerTask recordOperation(OperationLogVo operationLog) {return new TimerTask() {@Overridepublic void run() {// 找到日志服务bean,进行日志持久化操作SpringUtils.getBean(OperationLogService.class).saveOperationLog(operationLog);}};}}

 这里的OperationLogService就是日志服务类,我们可以在里面进行日志信息的入库等,具体内容要根据你的实际情况来调整,我这里是存入到msql数据库中持久化,源码仓库会在文末贴出,这里就不细讲了。

使用线程池异步执行日志记录操作

为了避免日志记录操作阻塞主线程,影响业务响应时间,我们可以使用线程池异步执行日志记录操作。在切面的最后,我们将构建好的OperationLogVo对象提交到线程池中,由工作线程异步完成日志的存储操作。

@Around("optLogPointCut()")public Object around(ProceedingJoinPoint joinPoint) {String methodName = joinPoint.getTarget().getClass().getSimpleName() + "." + joinPoint.getSignature().getName();OperationLogVo operationLogVo = null;try {operationLogVo = this.recordLog(joinPoint);} catch (IllegalStateException e) {log.error("no web request:{}", e.getMessage());}long startTime = System.currentTimeMillis();Object result = null;try {result = joinPoint.proceed();// 正常返回数据operationLogVo.setData(JSON.toJSONString(result));} catch (Throwable e) {log.info("method: {}, throws: {}", methodName, ExceptionUtils.getStackTrace(e));if (operationLogVo != null) {operationLogVo.setErrorMessage(stackTraceToString(e.getClass().getName(), e.getMessage(), e.getStackTrace()));}} finally {long endTime = System.currentTimeMillis();if (operationLogVo != null) {operationLogVo.setTimes(endTime - startTime);//异步记录操作日志AsyncManager.getInstance().execute(AsyncFactory.recordOperation(operationLogVo));}}return result;}

使用日志注解,测试

@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@PostMapping("/add")@OperationLog("添加用户")// 这里可以写上操作日志的描述public ResponseEntity<String> addUser(@RequestBody UserReq addReq) {return userService.addUser(addReq);}}

启动项目后,我们尝试插入一个用户,可以看到日志已经记录到了库中

异步日志记录的优缺点分析

优点

  1. 提高响应速度和系统吞吐量:通过异步记录日志,可以避免因日志记录操作中的I/O操作而阻塞主线程,从而提高系统的响应速度和处理能力。
  2. 解耦日志记录与业务逻辑:异步记录机制使得日志记录的逻辑与业务逻辑分离,有助于保持代码的整洁和易于维护。
  3. 提高系统的健壮性:在面对大量日志写入操作时,异步机制可以平滑处理高峰,避免系统因同步写入日志而出现性能瓶颈。

缺点

  1. 可能丢失日志:在极端情况下,如系统突然崩溃,可能会丢失还未来得及持久化的日志。
  2. 日志顺序无法保证:由于是异步操作,无法完全保证日志按照发生顺序进行记录,尤其是在高并发场景下。
  3. 增加系统复杂性:引入异步日志记录机制,增加了系统的复杂性,需要额外的线程管理和错误处理机制。

日志持久化方案分析

日志数据的持久化是确保操作记录可追溯和审计的重要环节,本文章使用的持久化方案是关系型数据库,当然还有很多其他的方案,常见的日志持久化方案包括:

  1. 关系型数据库:将日志数据存储在关系型数据库中,如MySQL、PostgreSQL等。这种方案便于日志的查询、管理和维护,但在高并发场景下可能会成为瓶颈。

  2. 日志文件:直接将日志写入文件系统,这种方式简单高效,适用于大部分场景。但需要合理规划日志的切割、备份和清理策略,以避免文件过大或过多导致的问题。

  3. 消息队列(如Kafka):将日志作为消息发送到Kafka等消息队列系统中,可以实现高吞吐量的日志处理。这种方案适用于日志量巨大且需要快速处理的场景,同时也便于实现日志数据的分布式处理和存储。

每种方案都有其适用场景和限制,实际选择时需要根据系统的具体需求和现有架构做出合理的决策。

总结

异步日志记录是一种提升系统性能和可维护性的有效手段,通过将日志记录操作异步化,不仅可以减少对业务处理流程的影响,还可以提高日志处理的灵活性和扩展性。然而,实现异步日志记录机制也伴随着一定的挑战,如日志的实时性、顺序性和丢失风险等问题。

在选择日志持久化方案时,应根据系统的实际需求考虑日志数据的安全性、查询效率、成本等因素,选择最适合的存储介质和技术方案。无论采取哪种方案,都应该注意日志系统的健壮性设计,确保日志数据的完整性和可靠性。

多线程编程系列的源码都放在我的github仓库啦,有需要的可以点点小star,感谢支持~

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

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

相关文章

C++笔记:从零开始一步步手撕高阶数据结构AVL树

文章目录 高度平衡二叉搜索树实现一颗AVL树结点与树的描述——定义类AVL树的插入操作步骤1&#xff1a;按照二叉搜索树的方法插入结点步骤2&#xff1a;自底向上调整平衡因子步骤3&#xff1a;触发旋转操作&#xff08;AVL树平衡的精髓&#xff09;右单旋左单旋左右双旋右左双旋…

TSINGSEE青犀视频AI方案:数据+算力+算法,人工智能的三大基石

背景分析 随着信息技术的迅猛发展&#xff0c;人工智能&#xff08;AI&#xff09;已经逐渐渗透到我们生活的各个领域&#xff0c;从智能家居到自动驾驶&#xff0c;从医疗诊断到金融风控&#xff0c;AI的应用正在改变着我们的生活方式。而数据、算法和算力&#xff0c;正是构成…

【矩阵】48. 旋转图像【中等】

旋转图像 给定一个 n n 的二维矩阵 matrix 表示一个图像。请你将图像顺时针旋转 90 度。你必须在 原地 旋转图像&#xff0c;这意味着你需要直接修改输入的二维矩阵。请不要 使用另一个矩阵来旋转图像。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,…

rust学习笔记(1-7)

原文 8万字带你入门Rust 1.包管理工具Cargo 新建项目 1&#xff09;打开 cmd 输入命令查看 cargo 版本 cargo --version2&#xff09; 使用 cargo new 项目名 在文件夹&#xff0c;按 shift 鼠标右键 &#xff0c;打开命令行&#xff0c;运行如下命令&#xff0c;即可创建…

基于springboot的七彩云南文化旅游网站的设计与实现(论文+源码)_kaic

摘 要 传统办法管理信息首先需要花费的时间比较多&#xff0c;其次数据出错率比较高&#xff0c;而且对错误的数据进行更改也比较困难&#xff0c;最后&#xff0c;检索数据费事费力。因此&#xff0c;在计算机上安装七彩云南文化旅游网站软件来发挥其高效地信息处理的作用&am…

【使用postman测试python接口】

打开python服务 设置postman如下&#xff0c;并发送&#xff1a; postman新建请求设置请求方式为post设置地址、raw、json方式、内容如下 结果&#xff1a; python如下&#xff1a; from flask import Flask, request, jsonifyapp Flask(__name__) # 实例化对象app.route…

你为什么是你,而不是别人?认识人格的力量

你为什么是你&#xff0c;而不是别人&#xff1f;让你做自我介绍&#xff0c;你会怎么描述自己呢&#xff1f; 人格心理学是心理学的一门重要分支学科。探求、描述和揭示个体思想、情绪及行为的独特模式&#xff0c;综合个人与环境诸多影响因素&#xff0c;对现实社会中的个人作…

Vite为什么比Webpack快

一、引言 主流的前端构建工具包括以下几种&#xff1a; Webpack&#xff1a;当下最热门的前端资源模块化管理和打包工具。它能够将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。同时&#xff0c;Webpack还支持代码分割&#xff0c;可以按需加载模块&#…

Redis底层数据结构之String

文章目录 1. 前提回顾2. RedisObject三大数据类型简介3. SDS字符串4. SDS字符串源码分析5. 总结 1. 前提回顾 前面我们说到redis的String数据结构在底层有多种编码方式。例如我们执行下面两条语句 set k1 v1 set age 17我们查看类型&#xff0c;发现这类型都是String类型 我们…

docker desktop 启动失败

docker desktop 启动失败 1.如果直接启动报错&#xff0c;设置 2.如果启动报 Failed to set version to docker-desktop: exit code: -1的解决方法. netsh winsock reset

vue3+Ts项目按需引入Echarts,并封装成hooks

记录 vue3Ts 项目中&#xff0c;按需引入echarts并进行二次封装使用。 1、安装&#xff1a;npm i echarts 2、新增按需引入配置文件&#xff1a;echartsConfig.ts // 引入 echarts 核心模块&#xff0c;核心模块提供了 echarts 使用必须要的接口。 import * as echarts from …

3.Windows下安装MongoDB和Compass教程

Windows下安装MongoDB 总体体验下来&#xff0c;&#xff0c;要比MySQL的安装简单了许多&#xff0c;没有过多的配置&#xff0c;直接就上手了&#xff01; 1、下载 进入官方的下载页面https://www.mongodb.com/try/download/community&#xff0c;如下选择&#xff0c;我选…

第七节:使用SMB发布Web前端程序

一、概述 一直以来&#xff0c;多数人都使用Apache、IIS、Tomcat等开源或商业Web服务器来运行Web程序&#xff0c;各种参数太多&#xff0c;与我们简单易用逻辑相左。所以在架构设计的时候&#xff0c;我们也在考虑&#xff0c;我们公司的Web程序是否能运行在SMB中&#xff0c;…

52 硬中断的实现

前言 呵呵 中断机制 也是内核中很常见的机制了 中断机制是现代计算机系统中的基本机制之一&#xff0c;它在系统中起着通信网络的作用&#xff0c;以协调系统对各种外部事件的响应和处理&#xff0c;中断是实现多道程序设计的必要条件&#xff0c;中断是CPU 对系统发生的某个…

idea Springboot 在线考试管理系统开发mysql数据库web结构java编程计算机网页

一、源码特点 springboot 在线考试管理系统是一套完善的完整信息系统&#xff0c;结合mvc框架和bootstrap完成本系统springboot spring mybatis &#xff0c;对理解JSP java编程开发语言有帮助系统采用springboot框架&#xff08;MVC模式开发&#xff09;&#xff0c;系统具有…

关于-机器人学导论

机器人学导论&#xff0c;经典的基础教材&#xff0c;原书叫introduction to robotics mechanics and control &#xff0c;在网上看到这个下载&#xff0c;这个好像不像有些书已经免费了&#xff0c;但是太经典拿它当教材的很多&#xff0c;最好看清晰的纸质版&#xff0c;特别…

java-ssm-jsp基于java的信访管理系统的设计与实现

java-ssm-jsp基于java的信访管理系统的设计与实现 获取源码——》公主号&#xff1a;计算机专业毕设大全 获取源码——》公主号&#xff1a;计算机专业毕设大全

Gitlab CI/CD 自动化打包部署前端(vue)项目

一、虚拟机安装 1.vmware下载 2.镜像下载 3.Ubuntu 4.新建虚拟机 一直点下一步&#xff0c;直到点击完成。 5.分配镜像 二、Gitlab CI/CD 自动化部署项目 1.配置GitLab CI/CD&#xff1a; A.在你的Vue.js项目中&#xff0c;创建一个名为.gitlab-ci.yml的文件&#xff0…

windbg调试协议wireshark抓包解析插件

把目录下文件复制到如下位置,Wireshark支持版本4.0以上 C:\Program Files\Wireshark\plugins\4.0\kdnet.lua C:\Program Files\Wireshark\gcrypt.dll C:\Program Files\Wireshark\luagcrypt.dll 启动 “C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe” -k …

使用vscode——配置vue3用户代码片段

一、 设置/配置用户代码片段 二、点击新建全局代码片段文件输入vue.json 三、配置代码片段、 {"Print to console": {"prefix": "vue3","body": ["<template>"," <div></div>","</te…