在企业级 Java 应用开发中,操作日志记录是一个不可或缺的功能。它不仅能追踪用户行为、排查系统问题,还是数据审计的重要依据。若依 (RuoYi) 框架作为一款成熟的 Java 快速开发平台,其日志记录功能基于 AOP (面向切面编程) 实现,既灵活又不侵入业务代码。本文将深入剖析若依框架的 AOP 日志切片实现原理,带你理解其设计思想与具体代码实现。
一、AOP 日志记录的核心优势
在传统开发模式中,我们可能会在每个业务方法中硬编码日志记录逻辑,这样做存在诸多问题:
- 代码侵入性强:日志逻辑与业务逻辑混杂,违背单一职责原则
- 维护成本高:修改日志格式需改动所有相关方法
- 扩展性差:新增日志需求时需逐个调整方法
而若依框架采用 AOP 实现日志记录,完美解决了这些问题:
- 解耦:日志记录与业务逻辑完全分离
- 复用:一套日志逻辑可应用于多个方法
- 灵活:通过注解控制哪些方法需要记录日志
- 无侵入:无需修改业务方法代码即可增强功能
二、若依日志切片的核心组件
若依框架的日志记录功能主要由三个部分组成:
- 自定义注解
@Log
:标记需要记录日志的方法 - AOP 切片
LogAspect
:拦截被@Log
标记的方法,执行日志记录逻辑 - 日志实体与存储:
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
的配合流程:
LogAspect
在拦截@Log
注解方法时,已封装好SysOperLog
实体(包含用户、方法、参数等信息)。- 通过
AsyncFactory.recordOper(operLog)
创建异步任务,任务中仅需补充地理位置信息并写入数据库。 - 最终由
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);
}
}
工作流程:
LogAspect
通过AsyncFactory.recordOper(operLog)
创建TimerTask
任务。- 调用
AsyncManager.me().execute(task)
将任务提交到ScheduledExecutorService
线程池。 - 线程池中的空闲线程执行
TimerTask
的run
方法,完成日志写入。
这种设计的优势是:
- 线程池参数可配置(核心线程数、队列大小等),避免线程创建销毁的开销。
- 业务线程(处理 HTTP 请求的线程)无需等待日志写入完成,直接返回响应,提升接口性能。
四、完整日志记录流程:从注解到入库
结合@Log
注解、LogAspect
、AsyncFactory
和AsyncManager
,若依框架的日志记录完整流程如下:
用户调用带@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
的设计亮点
- 职责单一:仅负责创建日志任务,不涉及任务调度和执行,符合单一职责原则。
- 解耦设计:
- 与
LogAspect
解耦:切片只需创建SysOperLog
,无需关心如何写入数据库。 - 与线程池解耦:任务创建与提交分离,便于替换线程池实现(如改用
ThreadPoolExecutor
)。
- 与
- 性能优化:
- 异步执行避免阻塞主线程。
- 地理位置查询等耗时操作放在异步任务中,不影响业务响应。
- 扩展性强:如需新增日志类型(如系统日志、审计日志),只需在
AsyncFactory
中添加新的recordXxx
方法即可。
六、使用场景与最佳实践
-
必须加
@Log
注解吗?
是的。AsyncFactory.recordOper
方法仅负责执行任务,而任务所需的SysOperLog
实体依赖LogAspect
通过@Log
注解拦截并封装。没有@Log
注解,LogAspect
不会触发,日志任务也不会创建。 -
何时需要手动调用
AsyncFactory
?
仅在未使用@Log
注解的场景下(如登录、登出等非业务接口),才需要手动调用AsyncFactory.recordLogininfor
,例如:
// 登录成功后手动记录日志 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功"));
-
如何避免日志丢失?
若依框架通过数据库事务保证日志写入的可靠性,但异步任务仍可能因系统崩溃丢失。如需更高可靠性,可:- 改用消息队列(如 RabbitMQ)存储日志任务,确保持久化。
- 增加任务执行失败重试机制(
AsyncManager
可扩展重试逻辑)。
七、总结
若依框架的日志记录功能是 AOP 思想与异步编程的完美结合:
@Log
注解:标记需要记录日志的方法,定义日志元数据。LogAspect
切片:拦截方法执行,收集日志信息并封装为SysOperLog
。AsyncFactory
:创建异步任务,封装日志写入逻辑(查询地理位置、调用 Service)。AsyncManager
:将任务提交到线程池,实现异步执行,提升性能。
若依 AOP 日志实现的设计亮点
-
精细的日志粒度控制:通过
@Log
注解的属性(如isSaveRequestData
、excludeParamNames
),可灵活控制日志记录的内容,避免敏感信息泄露。 -
异步日志存储:采用异步方式保存日志,不阻塞业务流程,提升系统响应速度。
-
完整的生命周期覆盖:通过
@Before
、@AfterReturning
、@AfterThrowing
三大通知,覆盖了方法执行的全生命周期,确保日志的完整性。 -
线程安全的耗时计算:使用
ThreadLocal
存储方法开始时间,避免多线程环境下的时间计算混乱。 -
低侵入性:业务代码只需添加一个注解即可实现日志记录,无需修改原有业务逻辑。
这种设计既保证了日志记录的完整性和灵活性,又通过异步处理避免了对业务性能的影响,是企业级应用中日志模块的优秀实践。理解这一流程,不仅能帮助我们更好地使用若依框架,还能为自定义日志系统提供宝贵的参考。