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);
        // 获取request
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = Objects.requireNonNull(attributes).getRequest();
        // 日志保存到数据库
        OperationLogVo operationLogVo = new OperationLogVo();
        // 操作类型
        operationLogVo.setType(optLogger.value());
        // 请求URI
        operationLogVo.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() {
            @Override
            public 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 {

    @Autowired
    private 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,感谢支持~

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

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

相关文章

K8S日志收集方案-EFK部署

EFK架构工作流程 部署说明 ECK (Elastic Cloud on Kubernetes)&#xff1a;2.7 Kubernetes&#xff1a;1.23.0 文件准备 crds.yaml 下载地址&#xff1a;https://download.elastic.co/downloads/eck/2.7.0/crds.yaml operator.yaml 下载地址&#xff1a;https://download.e…

鸿蒙Harmony应用开发—ArkTS声明式开发(容器组件:FormLink)

提供静态卡片交互组件&#xff0c;用于静态卡片内部和提供方应用间的交互&#xff0c;当前支持router、message和call三种类型的事件。 说明&#xff1a; 该组件从API Version 10开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 该组件仅可…

Spring Cloud Alibaba微服务从入门到进阶(六)(声明式HTTP客户端-Feign)

Feign是Netflix开源的声明式HTTP客户端&#xff08;只要声明一个接口&#xff0c;Feign就会通过你定义的接口自动给你构造请求的目标地址&#xff0c;并帮助你请求&#xff09; 用Feign重构前面RestTemplate方式的服务间调用 想回顾一下RestTemplate调用 加依赖 项目集成Feig…

3. ElasticSearch搜索技术深入与聚合查询实战

1. ES分词器详解 1.1 基本概念 分词器官方称之为文本分析器&#xff0c;顾名思义&#xff0c;是对文本进行分析处理的一种手段&#xff0c;基本处理逻辑为按照预先制定的分词规则&#xff0c;把原始文档分割成若干更小粒度的词项&#xff0c;粒度大小取决于分词器规则。 1.2 …

米桃安全漏洞讲堂系列第4期:WebShell木马专题

一、WebShell概述 WebShell是黑客经常使用的一种恶意脚本也称为木马后门。其目的是获得对服务器的执行操作权限&#xff0c;比如执行系统命令、窃取用户文件、访问数据库、删改web页面等&#xff0c;其危害不言而喻。 黑客利用常见的漏洞&#xff0c;如文件上传、SQL注入、远程…

PMP和软考,考哪一个?

PMP跟软考有部分知识点是重合的&#xff0c;软考高项比较适用于计算机 IT 行业&#xff0c;而 PMP 不受行业限制&#xff0c;各行各业都适用&#xff0c;至于哪个更合适&#xff0c;看你想去国企还是民企&#xff0c;国企软考吃香&#xff0c;民企PMP 吃香 下面说下两者具体有什…

【四 (6)数据可视化之 Grafana安装、页面介绍、图表配置】

目录 文章导航一、Grafana介绍[✨ 特性]二、安装和配置1、安装2、权限配置&#xff08;账户/团队/用户&#xff09;①用户管理②团队管理③账户管理④看板权限 3、首选项配置4、插件管理①数据源插件②图表插件③应用插件④插件安装方式一⑤安装方式二 三、数据源管理1、添加数…

腾讯春招后端一面(八股篇)

前言 前几天在网上发了腾讯面试官问的一些问题&#xff0c;好多小伙伴关注&#xff0c;今天对这些问题写个具体答案&#xff0c;博主好久没看八股了&#xff0c;正好复习一下。 面试手撕了三道算法&#xff0c;这部分之后更&#xff0c;喜欢的小伙伴可以留意一下我的账号。 1…

JavaScript中的事件模型(详细案例代码)

文章目录 一、事件与事件流二、事件模型原始事件模型特性 标准事件模型特性 IE事件模型 一、事件与事件流 javascript中的事件&#xff0c;可以理解就是在HTML文档或者浏览器中发生的一种交互操作&#xff0c;使得网页具备互动性&#xff0c; 常见的有加载事件、鼠标事件、自定…

详解命令docker run -d --name container_name -e TZ=Asia/Shanghai your_image

docker run 是Docker的主要命令&#xff0c;用于从镜像启动一个新的容器。下面详细解释并举例说明 -d, --name, -e TZ 参数的用法&#xff1a; -d 或 --detach&#xff1a; 这个标志告诉Docker以守护进程&#xff08;后台&#xff09;模式运行容器。这意味着当你执行 docker ru…

JavaScript进阶:js的一些学习笔记-this指向,call,apply,bind,防抖,节流

文章目录 1. this指向1. 箭头函数 this的指向 2. 改变this的指向1. call()2. apply()3. bind() 3. 防抖和节流1. 防抖2. 节流 1. this指向 1. 箭头函数 this的指向 箭头函数默认帮我们绑定外层this的值&#xff0c;所以在箭头函数中this的值和外层的this是一样的箭头函数中的…

双碳目标下生态与农田系统温室气体排放模

当前全球温室气体大幅升高&#xff0c;过去170年CO2浓度上升47%&#xff0c;这种极速变化使得物种和生态系统的适应时间大大缩短&#xff0c;进而造成全球气候变暖、海平面上升、作物产量降低、人类心血管和呼吸道疾病加剧等种种危害。在此背景下&#xff0c;代表可持续发展的“…

linux ffmpeg编译

下载源码 https://ffmpeg.org/ csdn下载源码包 不想编译可以直接下载使用静态版本 https://ffmpeg.org/download.html https://johnvansickle.com/ffmpeg/ 根据cpu类型&#xff0c;下载解压后就可以直接使用了。 linux编译 安装底层依赖 yum install gcc yum isntall …

Openlayers入门教程 --- 万字长篇

也许你还不熟悉Openlayers&#xff0c;也许你是一个Openlayers小白&#xff0c;零基础没关系&#xff0c;这篇文章提供最基础的 Openlayers 教程&#xff0c;简单易学&#xff0c;贯穿整个Openlayers 知识体系。读完本文&#xff0c;您将会对 Openlayers 有一个全新的认识。 文…

FreeRTOS学习笔记

一、RTOS入门 1.RTOS介绍 RTOS全称&#xff1a;Real Time OS&#xff0c;实时操作系统。 特点&#xff1a; 分而治之&#xff1a;实现功能划分多个任务。延时函数&#xff1a;不会空等待&#xff0c;高优先级延时的时候执行低优先级&#xff0c;会让出CPU的使用权给其他任务…

Day43-2-企业级实时复制intofy介绍及实践

Day43-2-企业级实时复制intofy介绍及实践 1. 企业级备份方案介绍1.1 利用定时方式&#xff0c;实现周期备份重要数据信息。1.2 实时数据备份方案1.3 实时复制环境准备1.4 实时复制软件介绍1.5 实时复制inotify机制介绍1.6 项目部署实施1.6.1 部署环境准备1.6.2 检查Linux系统支…

Hive借助java反射解决User-agent编码乱码问题

一、需求背景 在截取到浏览器user-agent&#xff0c;并想保存入数据库中&#xff0c;经查询发现展示的为编码后的结果。 现需要经过url解码过程&#xff0c;将解码后的结果保存进数据库&#xff0c;那么有几种实现方式。 二、问题解决 1、百度&#xff1a;url在线解码工具 …

Hello,Spider!入门第一个爬虫程序

在各大编程语言中&#xff0c;初学者要学会编写的第一个简单程序一般就是“Hello, World!”&#xff0c;即通过程序来在屏幕上输出一行“Hello, World!”这样的文字&#xff0c;在Python中&#xff0c;只需一行代码就可以做到。我们把这第一个爬虫就称之为“HelloSpider”&…

免费分享一套SpringBoot+Vue自习室(预约)管理系统,帅呆了~~

大家好&#xff0c;我是java1234_小锋老师&#xff0c;看到一个不错的SpringBootVue自习室预约)管理系统&#xff0c;分享下哈。 项目视频演示 【免费】SpringBootVue自习室预约(预约)管理系统 Java毕业设计_哔哩哔哩_bilibili【免费】SpringBootVue自习室预约(预约)管理系统…

flask库

文章目录 flask库1. 基本使用2. 路由路径和路由参数3. 请求跳转和请求参数4. 模板渲染1. 模板变量2. 过滤器3. 测试器 5. 钩子函数与响应对象 flask库 flask是python编写的轻量级框架&#xff0c;提供Werkzeug&#xff08;WSGI工具集&#xff09;和jinjia2&#xff08;渲染模板…