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

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
云原生数据库 PolarDB PostgreSQL 版,标准版 2核4GB 50GB
云原生数据库 PolarDB MySQL 版,通用型 2核4GB 50GB
简介: Java多线程实战-异步操作日志记录解决方案(AOP+注解+多线程)

前言


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


日志记录代码和业务逻辑代码高度耦合,不利于代码的可维护性。

新增或修改日志记录需求时,需要修改多处代码,工作量较大。

由于日志记录操作通常需要进行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);
    }
 
 
}

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

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

优点

提高响应速度和系统吞吐量:通过异步记录日志,可以避免因日志记录操作中的I/O操作而阻塞主线程,从而提高系统的响应速度和处理能力。

解耦日志记录与业务逻辑:异步记录机制使得日志记录的逻辑与业务逻辑分离,有助于保持代码的整洁和易于维护。

提高系统的健壮性:在面对大量日志写入操作时,异步机制可以平滑处理高峰,避免系统因同步写入日志而出现性能瓶颈。

缺点

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


日志持久化方案分析

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


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


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


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


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

总结


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


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


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

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
5天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
11天前
|
安全 Java 开发者
深入解读JAVA多线程:wait()、notify()、notifyAll()的奥秘
在Java多线程编程中,`wait()`、`notify()`和`notifyAll()`方法是实现线程间通信和同步的关键机制。这些方法定义在`java.lang.Object`类中,每个Java对象都可以作为线程间通信的媒介。本文将详细解析这三个方法的使用方法和最佳实践,帮助开发者更高效地进行多线程编程。 示例代码展示了如何在同步方法中使用这些方法,确保线程安全和高效的通信。
35 9
|
8天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
11天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
6月前
|
安全 Java 编译器
Java其他: 什么是Java中的注解(Annotation)?
Java其他: 什么是Java中的注解(Annotation)?
80 0
|
SQL XML SpringCloudAlibaba
Java独有特性:注解(annotation)
注解(Annotation),也叫元数据。一种代码级别的说明。它是JDK1.5及以后版本引入的一个特性,与类、接口、枚举是在同一个层次。它可以声明在包、类、字段、方法、局部变量、方法参数等的前面,用来对这些元素进行说明,注释。它本身并不起任何作用,可以说有它没它都不影响程序的正常运行,注解的作用在于**「注解的处理程序」**,注解处理程序通过捕获
169 0
|
6月前
|
Java 编译器 开发者
Java注解(Annotation)技术深入解析
Java注解(Annotation)技术深入解析
449 1
|
6月前
|
Java
【Java】注解(Annotation)
【Java】注解(Annotation)
52 0
|
安全 Java 编译器
一文带你全面深入理解Java注解Annotation
一文带你全面深入理解Java注解Annotation
275 1
一文带你全面深入理解Java注解Annotation
|
Java 测试技术 程序员
Java的注解(Annotation)
Java的注解(Annotation)
133 0
下一篇
无影云桌面