深入浅出:若依 (RuoYi) 框架 AOP 切片实现日志记录的原理与实践

在企业级 Java 应用开发中,操作日志记录是一个不可或缺的功能。它不仅能追踪用户行为、排查系统问题,还是数据审计的重要依据。若依 (RuoYi) 框架作为一款成熟的 Java 快速开发平台,其日志记录功能基于 AOP (面向切面编程) 实现,既灵活又不侵入业务代码。本文将深入剖析若依框架的 AOP 日志切片实现原理,带你理解其设计思想与具体代码实现。

一、AOP 日志记录的核心优势

在传统开发模式中,我们可能会在每个业务方法中硬编码日志记录逻辑,这样做存在诸多问题:

  • 代码侵入性强:日志逻辑与业务逻辑混杂,违背单一职责原则
  • 维护成本高:修改日志格式需改动所有相关方法
  • 扩展性差:新增日志需求时需逐个调整方法

而若依框架采用 AOP 实现日志记录,完美解决了这些问题:

  • 解耦:日志记录与业务逻辑完全分离
  • 复用:一套日志逻辑可应用于多个方法
  • 灵活:通过注解控制哪些方法需要记录日志
  • 无侵入:无需修改业务方法代码即可增强功能

二、若依日志切片的核心组件

若依框架的日志记录功能主要由三个部分组成:

  1. 自定义注解@Log:标记需要记录日志的方法
  2. AOP 切片LogAspect:拦截被@Log标记的方法,执行日志记录逻辑
  3. 日志实体与存储SysOperLog实体类及数据库存储逻辑

下面我们逐一解析这些组件的实现细节。

1. 自定义注解@Log

@Log注解是连接业务方法与日志切片的桥梁,定义在com.ruoyi.common.annotation.Log路径下。它包含了日志记录所需的元数据:

package com.ruoyi.common.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.common.enums.OperatorType;

/**
 * 自定义操作日志记录注解
 * 
 * @author ruoyi
 *
 */
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log
{
    /**
     * 模块
     */
    public String title() default "";

    /**
     * 功能
     */
    public BusinessType businessType() default BusinessType.OTHER;

    /**
     * 操作人类别
     */
    public OperatorType operatorType() default OperatorType.MANAGE;

    /**
     * 是否保存请求的参数
     */
    public boolean isSaveRequestData() default true;

    /**
     * 是否保存响应的参数
     */
    public boolean isSaveResponseData() default true;

    /**
     * 排除指定的请求参数
     */
    public String[] excludeParamNames() default {};
}

注解属性说明

2.2 三大通知方法:拦截方法执行的全生命周期
若依通过三个 AOP 通知方法,实现了对目标方法执行过程的完整监控:

  • title:用于描述操作模块(如 "用户管理"),方便日志分类
  • businessType:枚举类型,标记操作类型(新增 / 修改 / 删除等)
  • operatorType:标记操作人身份(管理员 / 用户)
  • isSaveRequestData/isSaveResponseData:控制是否记录请求 / 响应数据
  • excludeParamNames:排除敏感参数(如密码),避免日志泄露敏感信息

    2. AOP 切片LogAspect的实现

    LogAspect是日志记录的核心逻辑实现类,位于com.ruoyi.framework.aspectj.LogAspect。它通过 AOP 的通知机制,在目标方法执行的不同阶段插入日志处理逻辑。

    2.1 切片的基本配置
    @Aspect  // 声明为切面类
    @Component  // 交给Spring容器管理
    public class LogAspect {
        private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
        
        /** 排除敏感属性字段(密码等) */
        public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
        
        /** 计算操作消耗时间的ThreadLocal */
        private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");
        
        // 通知方法...
    }

  • @Aspect注解声明这是一个 AOP 切面类
  • @Component确保该类被 Spring 扫描并管理
  • TIME_THREADLOCAL用于记录方法开始时间,计算执行耗时(线程安全)
① 前置通知(@Before):记录方法开始时间
 /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(controllerLog)")
    public void doBefore(JoinPoint joinPoint, Log controllerLog)
    {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }
  • @Before(value = "@annotation(controllerLog)")表示:当方法被@Log注解标记时,在方法执行前触发该通知
  • JoinPoint参数用于获取目标方法的信息(类名、方法名、参数等)
  • Log controllerLog参数绑定了目标方法上的@Log注解实例,可获取注解属性
    ② 后置通知(@AfterReturning):方法正常执行后记录日志
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult) {
        // 处理日志,传入响应结果(jsonResult)
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }
  • @AfterReturning在方法正常返回后执行
  • returning = "jsonResult"用于获取方法的返回值,可记录响应数据

 ③ 异常通知(@AfterThrowing):方法抛出异常时记录错误日志

@AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e) {
    // 处理日志,传入异常信息(e)
    handleLog(joinPoint, controllerLog, e, null);
}
  • @AfterThrowing在方法抛出异常时执行
  • throwing = "e"用于获取异常对象,记录错误信息
2.3 核心处理方法:handleLog组装日志信息

三个通知方法最终都会调用handleLog方法,完成日志信息的收集与存储:

protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) {
    try {
        // 1. 获取当前登录用户信息
        LoginUser loginUser = SecurityUtils.getLoginUser();
        
        // 2. 初始化日志实体
        SysOperLog operLog = new SysOperLog();
        operLog.setStatus(BusinessStatus.SUCCESS.ordinal()); // 默认成功状态
        
        // 3. 收集请求相关信息
        String ip = IpUtils.getIpAddr(); // 获取客户端IP
        operLog.setOperIp(ip);
        operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255)); // 请求URL
        
        // 4. 设置操作人信息
        if (loginUser != null) {
            operLog.setOperName(loginUser.getUsername());
            SysUser currentUser = loginUser.getUser();
            if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept())) {
                operLog.setDeptName(currentUser.getDept().getDeptName()); // 部门名称
            }
        }
        
        // 5. 处理异常信息(若有)
        if (e != null) {
            operLog.setStatus(BusinessStatus.FAIL.ordinal()); // 标记为失败
            // 记录异常信息(截断过长的信息)
            operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000));
        }
        
        // 6. 设置方法信息
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = joinPoint.getSignature().getName();
        operLog.setMethod(className + "." + methodName + "()"); // 类名.方法名()
        
        // 7. 设置请求方式(GET/POST等)
        operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
        
        // 8. 从@Log注解中获取日志描述信息
        getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
        
        // 9. 计算方法执行耗时
        operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
        
        // 10. 异步保存日志(避免阻塞主线程)
        AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
    } catch (Exception exp) {
        log.error("日志记录异常:{}", exp.getMessage());
        exp.printStackTrace();
    } finally {
        // 清除ThreadLocal,避免内存泄漏
        TIME_THREADLOCAL.remove();
    }
}

handleLog方法的核心逻辑可归纳为:

  • 收集用户信息(操作人、部门)
  • 收集请求信息(IP、URL、请求方式)
  • 收集方法信息(类名、方法名、执行耗时)
  • 处理异常状态(若有异常则标记失败并记录错误信息)
  • @Log注解中获取业务相关信息(标题、业务类型等)
  • 异步保存日志(通过AsyncManager实现异步执行,不影响主线程)
2.4 辅助方法:完善日志细节
① getControllerMethodDescription:解析@Log注解属性
public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception {
    operLog.setBusinessType(log.businessType().ordinal()); // 业务类型(枚举转数字)
    operLog.setTitle(log.title()); // 日志标题
    operLog.setOperatorType(log.operatorType().ordinal()); // 操作人类型
    
    // 保存请求参数(如果@Log注解配置了isSaveRequestData=true)
    if (log.isSaveRequestData()) {
        setRequestValue(joinPoint, operLog, log.excludeParamNames());
    }
    
    // 保存响应数据(如果@Log注解配置了isSaveResponseData=true)
    if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult)) {
        operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000)); // 截断过长的响应数据
    }
}
② setRequestValue:处理请求参数(过滤敏感信息)
private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception {
    // 获取请求参数Map
    Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
    String requestMethod = operLog.getRequestMethod();
    
    // 处理POST/PUT/DELETE等方法的参数(可能在请求体中)
    if (StringUtils.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name())) {
        String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
        operLog.setOperParam(StringUtils.substring(params, 0, 2000));
    } else {
        // 过滤敏感参数后,将参数转为JSON字符串
        operLog.setOperParam(StringUtils.substring(
            JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 
            0, 2000
        ));
    }
}

该方法会:

  • 获取请求参数(包括 URL 参数和请求体参数)
  • 过滤敏感信息(密码等,通过excludePropertyPreFilter实现)
  • 截断过长的参数信息(避免日志表字段溢出)

完整代码:

package com.ruoyi.framework.aspectj;

import java.util.Collection;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.lang3.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.NamedThreadLocal;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.web.multipart.MultipartFile;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.annotation.Log;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.text.Convert;
import com.ruoyi.common.enums.BusinessStatus;
import com.ruoyi.common.enums.HttpMethod;
import com.ruoyi.common.filter.PropertyPreExcludeFilter;
import com.ruoyi.common.utils.ExceptionUtil;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.ip.IpUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.domain.SysOperLog;

/**
 * 操作日志记录处理
 * 
 * @author ruoyi
 */
@Aspect
@Component
public class LogAspect
{
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);

    /** 排除敏感属性字段 */
    public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };

    /** 计算操作消耗时间 */
    private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time");

    /**
     * 处理请求前执行
     */
    @Before(value = "@annotation(controllerLog)")
    public void doBefore(JoinPoint joinPoint, Log controllerLog)
    {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "@annotation(controllerLog)", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Log controllerLog, Object jsonResult)
    {
        handleLog(joinPoint, controllerLog, null, jsonResult);
    }

    /**
     * 拦截异常操作
     * 
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "@annotation(controllerLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Log controllerLog, Exception e)
    {
        handleLog(joinPoint, controllerLog, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult)
    {
        try
        {
            // 获取当前的用户
            LoginUser loginUser = SecurityUtils.getLoginUser();

            // *========数据库日志=========*//
            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
            // 请求的地址
            String ip = IpUtils.getIpAddr();
            operLog.setOperIp(ip);
            operLog.setOperUrl(StringUtils.substring(ServletUtils.getRequest().getRequestURI(), 0, 255));
            if (loginUser != null)
            {
                operLog.setOperName(loginUser.getUsername());
                SysUser currentUser = loginUser.getUser();
                if (StringUtils.isNotNull(currentUser) && StringUtils.isNotNull(currentUser.getDept()))
                {
                    operLog.setDeptName(currentUser.getDept().getDeptName());
                }
            }

            if (e != null)
            {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(Convert.toStr(e.getMessage(), ExceptionUtil.getExceptionMessage(e)), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 设置请求方式
            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog, jsonResult);
            // 设置消耗时间
            operLog.setCostTime(System.currentTimeMillis() - TIME_THREADLOCAL.get());
            // 保存数据库
            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
        }
        catch (Exception exp)
        {
            // 记录本地异常日志
            log.error("异常信息:{}", exp.getMessage());
            exp.printStackTrace();
        }
        finally
        {
            TIME_THREADLOCAL.remove();
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     * 
     * @param log 日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog, Object jsonResult) throws Exception
    {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
        // 设置操作人类别
        operLog.setOperatorType(log.operatorType().ordinal());
        // 是否需要保存request,参数和值
        if (log.isSaveRequestData())
        {
            // 获取参数的信息,传入到数据库中。
            setRequestValue(joinPoint, operLog, log.excludeParamNames());
        }
        // 是否需要保存response,参数和值
        if (log.isSaveResponseData() && StringUtils.isNotNull(jsonResult))
        {
            operLog.setJsonResult(StringUtils.substring(JSON.toJSONString(jsonResult), 0, 2000));
        }
    }

    /**
     * 获取请求的参数,放到log中
     * 
     * @param operLog 操作日志
     * @throws Exception 异常
     */
    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception
    {
        Map<?, ?> paramsMap = ServletUtils.getParamMap(ServletUtils.getRequest());
        String requestMethod = operLog.getRequestMethod();
        if (StringUtils.isEmpty(paramsMap) && StringUtils.equalsAny(requestMethod, HttpMethod.PUT.name(), HttpMethod.POST.name(), HttpMethod.DELETE.name()))
        {
            String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
        }
        else
        {
            operLog.setOperParam(StringUtils.substring(JSON.toJSONString(paramsMap, excludePropertyPreFilter(excludeParamNames)), 0, 2000));
        }
    }

    /**
     * 参数拼装
     */
    private String argsArrayToString(Object[] paramsArray, String[] excludeParamNames)
    {
        String params = "";
        if (paramsArray != null && paramsArray.length > 0)
        {
            for (Object o : paramsArray)
            {
                if (StringUtils.isNotNull(o) && !isFilterObject(o))
                {
                    try
                    {
                        String jsonObj = JSON.toJSONString(o, excludePropertyPreFilter(excludeParamNames));
                        params += jsonObj.toString() + " ";
                    }
                    catch (Exception e)
                    {
                    }
                }
            }
        }
        return params.trim();
    }

    /**
     * 忽略敏感属性
     */
    public PropertyPreExcludeFilter excludePropertyPreFilter(String[] excludeParamNames)
    {
        return new PropertyPreExcludeFilter().addExcludes(ArrayUtils.addAll(EXCLUDE_PROPERTIES, excludeParamNames));
    }

    /**
     * 判断是否需要过滤的对象。
     * 
     * @param o 对象信息。
     * @return 如果是需要过滤的对象,则返回true;否则返回false。
     */
    @SuppressWarnings("rawtypes")
    public boolean isFilterObject(final Object o)
    {
        Class<?> clazz = o.getClass();
        if (clazz.isArray())
        {
            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
        }
        else if (Collection.class.isAssignableFrom(clazz))
        {
            Collection collection = (Collection) o;
            for (Object value : collection)
            {
                return value instanceof MultipartFile;
            }
        }
        else if (Map.class.isAssignableFrom(clazz))
        {
            Map map = (Map) o;
            for (Object value : map.entrySet())
            {
                Map.Entry entry = (Map.Entry) value;
                return entry.getValue() instanceof MultipartFile;
            }
        }
        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
                || o instanceof BindingResult;
    }
}

若依框架的日志记录功能使用非常简单,只需在需要记录日志的 Controller 方法上添加@Log注解即可:

@RestController
@RequestMapping("/system/user")
public class SysUserController {

    @Autowired
    private ISysUserService userService;

    /**
     * 新增用户
     */
    @PreAuthorize("@ss.hasPermi('system:user:add')")
    @Log(title = "用户管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@Validated @RequestBody SysUser user) {
        if (UserConstants.NOT_UNIQUE.equals(userService.checkUserNameUnique(user.getUserName()))) {
            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,用户名已存在");
        } else if (StringUtils.isNotEmpty(user.getPhonenumber()) 
                && UserConstants.NOT_UNIQUE.equals(userService.checkPhoneUnique(user))) {
            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,手机号码已存在");
        } else if (StringUtils.isNotEmpty(user.getEmail()) 
                && UserConstants.NOT_UNIQUE.equals(userService.checkEmailUnique(user))) {
            return AjaxResult.error("新增用户'" + user.getUserName() + "'失败,邮箱已存在");
        }
        user.setCreateBy(SecurityUtils.getUsername());
        return toAjax(userService.insertUser(user));
    }
}

AsyncFactory位于com.ruoyi.framework.manager.factory.AsyncFactory,它的核心作用是创建日志记录的异步任务(TimerTask,并通过AsyncManager提交到线程池执行。这种设计的目的是:将日志记录与业务逻辑解耦,避免 I/O 操作(如数据库写入)阻塞主线程,提升系统响应速度

① 登录日志记录:recordLogininfor方法

该方法用于创建 "登录 / 登出 / 注册" 等用户行为的日志记录任务,核心代码如下:

public static TimerTask recordLogininfor(final String username, final String status, final String message, final Object... args) {
    // 解析User-Agent获取客户端信息(浏览器、操作系统)
    final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
    // 获取客户端IP地址
    final String ip = IpUtils.getIpAddr();
    
    // 返回一个TimerTask任务,run方法中包含具体的日志记录逻辑
    return new TimerTask() {
        @Override
        public void run() {
            // 1. 查询IP对应的地理位置(通过AddressUtils远程查询)
            String address = AddressUtils.getRealAddressByIP(ip);
            
            // 2. 打印日志到控制台/文件(便于实时查看)
            StringBuilder s = new StringBuilder();
            s.append(LogUtils.getBlock(ip)); // 格式化IP
            s.append(address); // 地理位置
            s.append(LogUtils.getBlock(username)); // 用户名
            s.append(LogUtils.getBlock(status)); // 操作状态(成功/失败)
            s.append(LogUtils.getBlock(message)); // 操作消息
            sys_user_logger.info(s.toString(), args); // 输出日志
            
            // 3. 封装登录日志实体(SysLogininfor)
            SysLogininfor logininfor = new SysLogininfor();
            logininfor.setUserName(username);
            logininfor.setIpaddr(ip);
            logininfor.setLoginLocation(address); // IP对应的地理位置
            logininfor.setBrowser(userAgent.getBrowser().getName()); // 浏览器类型
            logininfor.setOs(userAgent.getOperatingSystem().getName()); // 操作系统
            logininfor.setMsg(message); // 操作详情(如"登录成功"、"密码错误")
            
            // 4. 设置日志状态(成功/失败)
            if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
                logininfor.setStatus(Constants.SUCCESS); // 成功状态
            } else if (Constants.LOGIN_FAIL.equals(status)) {
                logininfor.setStatus(Constants.FAIL); // 失败状态
            }
            
            // 5. 异步写入数据库(通过SpringUtils获取Service实例)
            SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
        }
    };
}

核心功能

  • 解析客户端User-Agent头,获取浏览器类型(如 Chrome)和操作系统(如 Windows 10)。
  • 通过 IP 地址查询地理位置(基于AddressUtils,内部可能调用第三方 IP 库或接口)。
  • 封装SysLogininfor实体,记录登录用户、IP、位置、操作结果等信息。
  • 调用ISysLogininforService将日志写入数据库(sys_logininfor表)。
② 操作日志记录:recordOper方法

该方法用于创建业务操作日志(如新增用户、修改角色等)的记录任务,是与@Log注解配合的核心方法:

public static TimerTask recordOper(final SysOperLog operLog) {
    return new TimerTask() {
        @Override
        public void run() {
            // 1. 补充IP对应的地理位置(如果未在切面中设置)
            operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
            // 2. 调用Service写入操作日志到数据库
            SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
        }
    };
}

LogAspect的配合流程

  1. LogAspect在拦截@Log注解方法时,已封装好SysOperLog实体(包含用户、方法、参数等信息)。
  2. 通过AsyncFactory.recordOper(operLog)创建异步任务,任务中仅需补充地理位置信息并写入数据库。
  3. 最终由AsyncManager将任务提交到线程池,实现异步执行。

2.2 为什么用TimerTask

AsyncFactory的方法返回TimerTask而非普通Runnable,是因为若依框架的AsyncManager(异步任务管理器)内部基于ScheduledExecutorService实现,而TimerTask是该线程池支持的任务类型。其核心作用是:

  • 统一任务接口,便于AsyncManager管理(提交、取消等)。
  • 支持延迟执行(虽然日志记录通常无需延迟,但框架预留了扩展能力)。

三、AsyncManager:异步任务的 "调度中心"

AsyncFactory创建的任务需要通过AsyncManager提交到线程池执行。AsyncManager是若依框架的异步任务管理器,位于com.ruoyi.framework.manager.AsyncManager,核心代码如下:

public class AsyncManager {
    // 单例模式
    private static final AsyncManager INSTANCE = new AsyncManager();
    // 异步任务线程池
    private final ScheduledExecutorService executor = SpringUtils.getBean(ScheduledExecutorService.class);
    
    private AsyncManager() {}
    
    public static AsyncManager me() {
        return INSTANCE;
    }
    
    // 提交任务到线程池
    public void execute(TimerTask task) {
        // 立即执行任务(延迟0毫秒)
        executor.schedule(task, 0, TimeUnit.MILLISECONDS);
    }
}

工作流程

  1. LogAspect通过AsyncFactory.recordOper(operLog)创建TimerTask任务。
  2. 调用AsyncManager.me().execute(task)将任务提交到ScheduledExecutorService线程池。
  3. 线程池中的空闲线程执行TimerTaskrun方法,完成日志写入。

这种设计的优势是:

  • 线程池参数可配置(核心线程数、队列大小等),避免线程创建销毁的开销。
  • 业务线程(处理 HTTP 请求的线程)无需等待日志写入完成,直接返回响应,提升接口性能。

四、完整日志记录流程:从注解到入库

结合@Log注解、LogAspectAsyncFactoryAsyncManager,若依框架的日志记录完整流程如下:

 用户调用带@Log注解的Controller方法
    ↓
LogAspect的@Before通知:记录方法开始时间
    ↓
目标方法执行(如新增用户)
    ↓
LogAspect的@AfterReturning/@AfterThrowing通知:
    1. 收集用户、方法、参数、结果/异常等信息
    2. 封装为SysOperLog实体
    3. 调用AsyncFactory.recordOper(operLog)创建异步任务
    ↓
AsyncManager.me().execute(task):将任务提交到线程池
    ↓
线程池执行任务(TimerTask的run方法):
    1. 补充IP地理位置
    2. 调用ISysOperLogService.insertOperlog(operLog)
    ↓
日志写入sys_oper_log表

五、AsyncFactory的设计亮点

  1. 职责单一:仅负责创建日志任务,不涉及任务调度和执行,符合单一职责原则。
  2. 解耦设计
    • LogAspect解耦:切片只需创建SysOperLog,无需关心如何写入数据库。
    • 与线程池解耦:任务创建与提交分离,便于替换线程池实现(如改用ThreadPoolExecutor)。
  3. 性能优化
    • 异步执行避免阻塞主线程。
    • 地理位置查询等耗时操作放在异步任务中,不影响业务响应。
  4. 扩展性强:如需新增日志类型(如系统日志、审计日志),只需在AsyncFactory中添加新的recordXxx方法即可。

六、使用场景与最佳实践

  1. 必须加@Log注解吗?
    是的。AsyncFactory.recordOper方法仅负责执行任务,而任务所需的SysOperLog实体依赖LogAspect通过@Log注解拦截并封装。没有@Log注解,LogAspect不会触发,日志任务也不会创建。

  2. 何时需要手动调用AsyncFactory
    仅在未使用@Log注解的场景下(如登录、登出等非业务接口),才需要手动调用AsyncFactory.recordLogininfor,例如:

// 登录成功后手动记录日志 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功"));

  1. 如何避免日志丢失?
    若依框架通过数据库事务保证日志写入的可靠性,但异步任务仍可能因系统崩溃丢失。如需更高可靠性,可:

    • 改用消息队列(如 RabbitMQ)存储日志任务,确保持久化。
    • 增加任务执行失败重试机制(AsyncManager可扩展重试逻辑)。

七、总结

若依框架的日志记录功能是 AOP 思想与异步编程的完美结合:

  • @Log注解:标记需要记录日志的方法,定义日志元数据。
  • LogAspect切片:拦截方法执行,收集日志信息并封装为SysOperLog
  • AsyncFactory:创建异步任务,封装日志写入逻辑(查询地理位置、调用 Service)。
  • AsyncManager:将任务提交到线程池,实现异步执行,提升性能。

若依 AOP 日志实现的设计亮点

  1. 精细的日志粒度控制:通过@Log注解的属性(如isSaveRequestDataexcludeParamNames),可灵活控制日志记录的内容,避免敏感信息泄露。

  2. 异步日志存储:采用异步方式保存日志,不阻塞业务流程,提升系统响应速度。

  3. 完整的生命周期覆盖:通过@Before@AfterReturning@AfterThrowing三大通知,覆盖了方法执行的全生命周期,确保日志的完整性。

  4. 线程安全的耗时计算:使用ThreadLocal存储方法开始时间,避免多线程环境下的时间计算混乱。

  5. 低侵入性:业务代码只需添加一个注解即可实现日志记录,无需修改原有业务逻辑。

这种设计既保证了日志记录的完整性和灵活性,又通过异步处理避免了对业务性能的影响,是企业级应用中日志模块的优秀实践。理解这一流程,不仅能帮助我们更好地使用若依框架,还能为自定义日志系统提供宝贵的参考。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值