RuoYi 登录性能:异步处理登录日志的实践与代码解析


优化 RuoYi 登录性能:异步处理登录日志的实践与代码解析

在现代Web应用中,用户体验和系统性能是至关重要的指标。RuoYi(若依)框架作为一款优秀的后台管理系统,其性能优化同样是开发者关注的焦点。在登录认证这样的高频操作中,即使是看似简单的日志记录,也可能成为潜在的性能瓶颈。本文将深入探讨在 RuoYi 框架中,如何通过异步处理登录日志来显著提升系统响应速度和吞吐量,并提供详细的代码解析。

一、为什么登录日志需要异步处理?

当用户尝试登录系统时,除了验证用户名密码、生成Token等核心业务逻辑外,通常还需要记录用户的登录信息,例如登录时间、IP地址、登录状态(成功/失败)等。这些信息往往需要持久化到数据库中。

如果这些日志记录操作是同步进行的,即在登录认证流程中等待数据库写入完成后才返回结果,那么:

  1. 增加响应时间: 数据库I/O操作相对耗时,会直接增加用户等待登录响应的时间。
  2. 降低并发能力: 在高并发场景下,同步的数据库操作会阻塞主线程,限制系统处理更多并发登录请求的能力。
  3. 资源竞争: 多个登录请求同时写入数据库时,可能引发数据库锁竞争,进一步恶化性能。

因此,将登录日志的写入操作从核心登录流程中剥离出来,进行异步处理,成为优化登录性能的关键策略。这意味着登录认证通过后,系统可以立即响应用户,而日志记录则在后台默默进行,互不干扰。

二、RuoYi 框架中异步处理登录日志的实现思路

在 RuoYi 框架中,我们通常会利用 Spring Boot 提供的特性,结合事件驱动机制和线程池来实现登录日志的异步记录。核心思路如下:

  1. 事件发布: 在登录认证成功或失败后,发布一个自定义的登录事件(LoginEvent),包含所有需要记录的登录信息。
  2. 异步监听: 创建一个事件监听器,并使用 Spring 的 @Async 注解将其标记为异步方法。当 LoginEvent 被发布时,这个监听器会在独立的线程中执行。
  3. 日志持久化: 在异步监听器中,调用 SysLogininforService 将登录信息保存到数据库。

三、代码实现与解析

接下来,我们将通过具体的代码示例来逐步构建异步登录日志功能。

1. 开启 Spring 异步支持

首先,确保你的 Spring Boot 应用开启了异步执行功能。这通常通过在启动类上添加 @EnableAsync 注解实现。

java 复制代码
// RuoYiApplication.java
package com.ruoyi;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.scheduling.annotation.EnableAsync; // 引入异步支持

/**
 * 启动程序
 *
 * @author ruoyi
 */
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })
@EnableAsync // 开启异步方法执行
public class RuoYiApplication
{
    public static void main(String[] args)
    {
        // System.setProperty("spring.devtools.restart.enabled", "false");
        SpringApplication.run(RuoYiApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  若依启动成功   ლ(´ڡ`ლ)゙  \n" +
                " .-------.         .--.  .--.\n" +
                " |  _ _   \\ .-.     |  |  |  |\n" +
                " | ( ' )  | `-'     |  .--.  |\n" +
                " |(_ o _) /         |  |  |  |\n" +
                " | (_,_).' __        |  |  |  |\n" +
                " |  |\\ \\  |  |   ___ |  |  |  |\n" +
                " |  | \\ `'   / / \\  \\          /\n" +
                " |  |  \\    /   \\  \\        /\n" +
                " '--'   `--'     '--'--.--'\n" +
                "                                      ");
    }
}

2. 定义异步线程池(可选,但推荐)

为了更好地控制异步任务的执行,我们可以自定义一个线程池供 @Async 注解使用。这样可以避免使用 Spring 默认的 SimpleAsyncTaskExecutor,提高线程资源管理效率。

java 复制代码
// AsyncConfig.java
package com.ruoyi.framework.config;

import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurer;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

/**
 * 异步任务配置
 *
 * @author ruoyi
 */
@Configuration
public class AsyncConfig implements AsyncConfigurer
{
    // 定义一个日志专用的线程池
    public static final String LOG_TASK_EXECUTOR = "logTaskExecutor";

    @Override
    @Bean(name = LOG_TASK_EXECUTOR)
    public Executor getAsyncExecutor()
    {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(5); // 核心线程数
        executor.setMaxPoolSize(10); // 最大线程数
        executor.setQueueCapacity(200); // 队列容量
        executor.setKeepAliveSeconds(60); // 线程空闲时间
        executor.setThreadNamePrefix("ruoyi-log-async-"); // 线程名称前缀
        // 拒绝策略:当队列满且线程数达到最大时,由调用者线程执行任务
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.initialize();
        return executor;
    }
}

3. 定义登录事件

创建一个自定义的 LoginEvent,它继承自 Spring 的 ApplicationEvent,用于封装登录相关的信息。

java 复制代码
// LoginEvent.java
package com.ruoyi.framework.event;

import com.ruoyi.system.domain.SysLogininfor;
import org.springframework.context.ApplicationEvent;

/**
 * 登录事件
 * 用于在登录成功或失败时,发布事件进行异步日志记录
 *
 * @author ruoyi
 */
public class LoginEvent extends ApplicationEvent
{
    private static final long serialVersionUID = 1L;

    private final SysLogininfor logininfor;

    public LoginEvent(Object source, SysLogininfor logininfor)
    {
        super(source);
        this.logininfor = logininfor;
    }

    public SysLogininfor getLogininfor()
    {
        return logininfor;
    }
}

4. 创建异步事件监听器

编写一个 Spring 组件作为事件监听器。使用 @EventListener 注解监听 LoginEvent,并通过 @Async 注解指定它在后台线程中执行,同时可以指定使用我们之前定义的线程池。

java 复制代码
// LoginEventListener.java
package com.ruoyi.framework.listener;

import com.ruoyi.framework.config.AsyncConfig;
import com.ruoyi.framework.event.LoginEvent;
import com.ruoyi.system.service.ISysLogininforService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;

/**
 * 登录事件监听器
 * 负责异步记录登录日志到数据库
 *
 * @author ruoyi
 */
@Component
public class LoginEventListener
{
    private static final Logger log = LoggerFactory.getLogger(LoginEventListener.class);

    @Autowired
    private ISysLogininforService logininforService;

    /**
     * 监听 LoginEvent,并在异步线程中处理登录日志
     * 指定使用 logTaskExecutor 线程池
     *
     * @param event 登录事件
     */
    @Async(AsyncConfig.LOG_TASK_EXECUTOR)
    @EventListener
    public void handleLoginEvent(LoginEvent event)
    {
        log.debug("异步线程[{}] 开始处理登录日志...", Thread.currentThread().getName());
        try
        {
            // 获取登录信息对象
            SysLogininfor logininfor = event.getLogininfor();
            // 调用服务层方法,将登录日志保存到数据库
            logininforService.insertLogininfor(logininfor);
            log.info("用户[{}] 登录日志记录成功。状态: {}", logininfor.getUserName(), logininfor.getStatus());
        }
        catch (Exception e)
        {
            log.error("异步记录登录日志失败:{}", e.getMessage(), e);
            // 可以在这里添加告警通知机制
        }
    }
}

5. 在登录服务中发布事件

最后,在 RuoYi 的登录服务(例如 SysLoginService)中,当用户登录成功或失败后,构建 SysLogininfor 对象,并通过 ApplicationEventPublisher 发布 LoginEvent

java 复制代码
// SysLoginService.java (简化版,仅展示事件发布逻辑)
package com.ruoyi.framework.web.service;

import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.exception.user.CaptchaException;
import com.ruoyi.common.exception.user.UserPasswordNotMatchException;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.framework.event.LoginEvent; // 引入 LoginEvent
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.domain.SysLogininfor;
import com.ruoyi.system.domain.SysUser;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationEventPublisher; // 引入事件发布器
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

/**
 * 登录校验方法
 *
 * @author ruoyi
 */
@Component
public class SysLoginService
{
    @Autowired
    private TokenService tokenService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private ISysUserService userService;

    @Autowired
    private ISysConfigService configService;

    @Autowired
    private ApplicationEventPublisher eventPublisher; // 注入事件发布器

    /**
     * 登录验证
     *
     * @param username 用户名
     * @param password 密码
     * @param code     验证码
     * @param uuid     唯一标识
     * @return 结果
     */
    public String login(String username, String password, String code, String uuid)
    {
        // ... (省略验证码、账户锁定等其他登录逻辑) ...

        SysLogininfor logininfor = new SysLogininfor();
        logininfor.setUserName(username);
        logininfor.setIpaddr(ServletUtils.getIpAddr(ServletUtils.getRequest()));
        logininfor.setLoginLocation(AsyncFactory.getRealAddressByIP(logininfor.getIpaddr()));
        logininfor.setBrowser(ServletUtils.getBrowser());
        logininfor.setOs(ServletUtils.getOs());
        logininfor.setLoginTime(DateUtils.getNowDate());

        Authentication authentication;
        try
        {
            // 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));

            // 认证成功
            logininfor.setStatus(Constants.SUCCESS);
            logininfor.setMsg("登录成功");
            // 发布登录成功事件
            eventPublisher.publishEvent(new LoginEvent(this, logininfor));
        }
        catch (Exception e)
        {
            logininfor.setStatus(Constants.FAIL);
            if (e instanceof BadCredentialsException || e instanceof UserPasswordNotMatchException)
            {
                logininfor.setMsg("用户不存在/密码错误");
            }
            else if (e instanceof CaptchaException)
            {
                logininfor.setMsg("验证码错误");
            }
            else
            {
                logininfor.setMsg(e.getMessage());
            }
            // 发布登录失败事件
            eventPublisher.publishEvent(new LoginEvent(this, logininfor));
            throw new RuntimeException(e.getMessage()); // 抛出异常给上层处理
        }
        // ... (省略后续生成Token等逻辑) ...
        return token;
    }
}

代码说明:

  • ApplicationEventPublisher 是 Spring 提供的事件发布接口,通过 eventPublisher.publishEvent() 方法可以发布任何继承自 ApplicationEvent 的事件。
  • LoginEvent 包含了所有需要在日志中记录的信息。
  • login 方法中认证成功或失败后,我们立即发布 LoginEvent,而无需等待数据库操作完成。
  • LoginEventListener 上的 @Async@EventListener 会确保 handleLoginEvent 方法在 logTaskExecutor 线程池中异步执行,从而不阻塞主登录流程。

四、测试与效果

通过上述改造,当用户发起登录请求时:

  1. 主线程快速完成用户认证和Token生成等核心操作。
  2. SysLoginService 发布 LoginEvent 后,立即返回登录结果给前端。
  3. 一个独立的线程(来自 logTaskExecutor 线程池)接收到 LoginEvent,并在后台异步执行 logininforService.insertLogininfor() 方法,将登录日志保存到数据库。

这种模式下,登录接口的响应速度将主要取决于认证逻辑和Token生成,而不再受数据库I/O的拖累。特别是在数据库压力较大或网络延迟较高的情况下,异步处理的优势将更加明显。

五、总结

异步处理登录日志是优化 RuoYi 框架性能的一种有效手段。通过利用 Spring 的 @EnableAsync、自定义线程池、事件发布与监听机制,我们可以轻松地将日志记录这一非核心业务逻辑从主流程中解耦出来,使其在后台异步执行。这不仅提升了用户登录体验,也增强了系统的并发处理能力和整体稳定性。

关键点回顾:

  • 问题根源: 同步数据库I/O是登录性能瓶颈。
  • 解决方案: 事件驱动 + @Async 异步处理。
  • 代码核心: LoginEvent@EnableAsync、自定义 Executor@Async @EventListenerApplicationEventPublisher

希望本文能帮助你在 RuoYi 框架的开发中更好地实践异步编程,构建出更高效、更健壮的应用。


`

相关推荐
Java水解15 分钟前
Spring Boot 配置文件深度解析
spring boot·后端
TT哇23 分钟前
Optional<T>
java·spring boot·java-ee
❀͜͡傀儡师1 小时前
Spring Boot函数式编程:轻量级路由函数替代传统Controller
java·spring boot·后端
Mr.朱鹏1 小时前
超时订单处理方案实战指南【完整版】
java·spring boot·redis·spring·rabbitmq·rocketmq·订单
Coder_Boy_2 小时前
SpringAI与LangChain4j的智能应用-(实践篇4)
java·人工智能·spring boot·langchain
用户3521802454752 小时前
🎉Spring Boot 3 + 多数据源 + Druid:监控页面 + 控制台 SQL 日志,终于搞定啦!
spring boot·微服务
while(1){yan}3 小时前
计算器和登录界面(实现前后端互通)
spring boot·spring·servlet·java-ee·tomcat·maven
xiaoshujiaa3 小时前
Java大厂面试实录:谢飞机硬刚互联网医疗微服务架构,Spring Cloud+Redis+Kafka全踩坑
spring boot·redis·微服务·kafka·flyway·java面试·互联网医疗
一 乐3 小时前
养老院信息|基于springboot + vue养老院信息管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端
天远Date Lab4 小时前
构建金融级信贷审批系统:Java Spring Boot 集成天远借贷行为验证 API 全指南
java·大数据·spring boot·金融