若依框架日志管理系统深度解析:从AOP切面到异步处理的完整实现

34 阅读10分钟

一、系统概述

1.1 功能简介

若依框架的日志管理系统提供:

  • 操作日志:记录业务操作(增删改查、导入导出等)
  • 登录日志:记录用户登录/登出
  • 异步处理:日志写入不阻塞主流程
  • 敏感信息过滤:自动过滤密码等敏感字段

1.2 日志类型

1.2.1 操作日志(SysOperLog)

记录业务操作,包含:

  • 操作模块、业务类型、操作人员
  • 请求URL、IP、地点
  • 请求参数、返回结果
  • 操作状态、错误信息
  • 执行时间、耗时

1.2.2 登录日志(SysLogininfor)

记录登录信息,包含:

  • 用户名、登录状态
  • IP、登录地点
  • 浏览器、操作系统
  • 登录时间、提示消息

二、架构设计

2.1 整体架构图

┌─────────────────────────────────────────────────────────┐
│                     Controller层                        │
│  ┌──────────────────────────────────────────────────┐   │
│  │  @Log注解标记需要记录日志的方法                    │   │
│  └──────────────────────────────────────────────────┘   │
└───────────────────┬─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────┐
│                    AOP切面层                             │
│  ┌──────────────────────────────────────────────────┐   │
│  │  LogAspect (操作日志切面)                         │   │
│  │  - @Before: 记录开始时间                          │   │
│  │  - @AfterReturning: 正常返回处理                  │   │
│  │  - @AfterThrowing: 异常处理                       │   │
│  └──────────────────────────────────────────────────┘   │
└───────────────────┬─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────┐
│                  异步任务管理层                          │
│  ┌──────────────────────────────────────────────────┐   │
│  │  AsyncManager (异步任务管理器)                    │   │
│  │  - 单例模式                                       │   │
│  │  - 延迟10ms执行                                   │   │
│  └──────────────────────────────────────────────────┘   │
│  ┌──────────────────────────────────────────────────┐   │
│  │  AsyncFactory (异步工厂)                          │   │
│  │  - recordOper(): 创建操作日志任务                  │   │
│  │  - recordLogininfor(): 创建登录日志任务            │   │
│  └──────────────────────────────────────────────────┘   │
└───────────────────┬─────────────────────────────────────┘
                    │
                    ▼
┌─────────────────────────────────────────────────────────┐
│                    数据持久层                            │
│  ┌──────────────────────────────────────────────────┐   │
│  │  Service层 → Mapper层 → 数据库                    │   │
│  └──────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────┘

2.2 技术栈

  • AOP:AspectJ
  • 异步处理:ScheduledExecutorService
  • 数据持久化:MyBatis
  • JSON处理:FastJSON2
  • IP地址解析:AddressUtils

三、核心组件详解

3.1 @Log注解

@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 {};
}

参数说明:

  • title:操作模块名称
  • businessType:业务类型(INSERT/UPDATE/DELETE/QUERY/EXPORT等)
  • operatorType:操作人类别(MANAGE/MOBILE/OTHER)
  • isSaveRequestData:是否保存请求参数
  • isSaveResponseData:是否保存响应参数
  • excludeParamNames:排除的参数名(如密码字段)

3.2 LogAspect(操作日志切面)

核心类:com.construct.framework.aspectj.LogAspect

3.2.1 关键属性

LogAspect.java Lines 54-58

@Before(value = "@annotation(controllerLog)")
public void boBefore(JoinPoint joinPoint, Log controllerLog)
{
    TIME_THREADLOCAL.set(System.currentTimeMillis());
}
  • EXCLUDE_PROPERTIES:默认排除的敏感字段(password、oldPassword等)
  • TIME_THREADLOCAL:ThreadLocal,记录方法开始时间

3.2.2 核心方法

  1. boBefore():方法执行前
  • 记录开始时间到ThreadLocal
  1. doAfterReturning():方法正常返回后
  • 调用handleLog()处理日志
  1. doAfterThrowing():方法抛出异常后
  • 调用handleLog(),传入异常信息
  1. handleLog():统一日志处理

LogAspect.java Lines 83-130

   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(), 0255));
            if (loginUser != null)
            {
                operLog.setOperName(loginUser.getUser().getNickName());
            }
            if (e != null)
            {
                operLog.setStatus(BusinessStatus.FAIL.ordinal());
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 02000));
            }
            // 设置方法名称
            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();
        }
    }

处理流程:

(1) 获取当前登录用户

(2) 创建SysOperLog对象

(3) 设置IP、URL、操作人

(4) 处理异常信息(如有)

(5) 设置方法名、请求方式

(6) 解析注解参数

(7) 计算耗时

(8) 异步保存日志

  1. getControllerMethodDescription():解析注解参数

LogAspect.java ines 139-158

    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), 02000));
        }
    }
  1. setRequestValue():获取并处理请求参数

LogAspect.java Lines 166-180

  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)
                && (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod)))
        {
            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));
        }
    }
  1. isFilterObject():过滤不需要记录的对象

LogAspect.java Lines 222-249

        @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;
    }

过滤对象:

  • MultipartFile(文件上传)
  • HttpServletRequestHttpServletResponse
  • BindingResult(参数校验结果)

3.3 AsyncManager(异步任务管理器)

AsyncManager.java Lines 15-56

public class AsyncManager
{
    /**
     * 操作延迟10毫秒
     */
    private final int OPERATE_DELAY_TIME = 10;

    /**
     * 异步操作任务调度线程池
     */
    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");

    /**
     * 单例模式
     */
    private AsyncManager(){}

    private static AsyncManager me = new AsyncManager();

    public static AsyncManager me()
    {
        return me;
    }

    /**
     * 执行任务
     * 
     * @param task 任务
     */
    public void execute(TimerTask task)
    {
        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 停止任务线程池
     */
    public void shutdown()
    {
        Threads.shutdownAndAwaitTermination(executor);
    }
}

特点:

  • 单例模式
  • 延迟10ms执行,避免影响主流程
  • 使用ScheduledExecutorService执行任务

3.4 AsyncFactory(异步工厂)

AsyncFactory.java Lines 99-111

public static TimerTask recordOper(final SysOperLog operLog)
{
    return new TimerTask()
    {
        @Override
        public void run()
        {
            // 远程查询操作地点
            operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
            SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
        }
    };
}

功能:

  • recordOper():创建操作日志任务
    • 根据IP查询操作地点
    • 调用Service保存日志
  • recordLogininfor():创建登录日志任务
    • 解析User-Agent获取浏览器和操作系统
    • 根据IP查询登录地点
    • 保存登录日志

3.5 线程池配置

ThreadPoolConfig.java Lines 45-62

    /**
     * 执行周期性或定时任务
     */
    @Bean(name = "scheduledExecutorService")
    protected ScheduledExecutorService scheduledExecutorService()
    {
       return new ScheduledThreadPoolExecutor(corePoolSize,
              new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build(),
              new ThreadPoolExecutor.CallerRunsPolicy())
        {
            @Override
            protected void afterExecute(Runnable r, Throwable t)
            {
                super.afterExecute(r, t);
                Threads.printException(r, t);
            }
        };
    }

配置参数:

  • 核心线程数:50
  • 最大线程数:200
  • 队列容量:1000
  • 线程命名:schedule-pool-%d
  • 守护线程:是
  • 拒绝策略:CallerRunsPolicy

3.6 实体类

3.6.1 SysOperLog(操作日志实体)

主要字段:

  • operId:日志主键
  • title:操作模块
  • businessType:业务类型(0其它 1新增 2修改 3删除等)
  • method:请求方法(类名.方法名)
  • requestMethod:请求方式(GET/POST等)
  • operatorType:操作类别(0其它 1后台用户 2手机端用户)
  • operName:操作人员
  • operUrl:请求URL
  • operIp:操作IP
  • operLocation:操作地点
  • operParam:请求参数(JSON,最大2000字符)
  • jsonResult:返回参数(JSON,最大2000字符)
  • status:操作状态(0正常 1异常)
  • errorMsg:错误消息(最大2000字符)
  • operTime:操作时间
  • costTime:消耗时间(毫秒)

四、实现流程

4.1 操作日志记录流程

1. Controller方法执行
   ↓
2. @Log注解触发AOP切面
   ↓
3. LogAspect.boBefore()
   - 记录开始时间到ThreadLocal
   ↓
4. 业务方法执行
   ↓
5. LogAspect.doAfterReturning() 或 doAfterThrowing()
   ↓
6. LogAspect.handleLog()
   - 获取用户信息
   - 创建SysOperLog对象
   - 设置基本信息(IP、URL、方法名等)
   - 解析@Log注解参数
   - 处理请求参数和响应结果
   - 计算执行耗时
   ↓
7. AsyncManager.execute()
   - 延迟10ms执行
   ↓
8. AsyncFactory.recordOper()
   - 根据IP查询操作地点
   - 调用Service保存日志
   ↓
9. SysOperLogService.insertOperlog()
   - 保存到数据库

4.2 登录日志记录流程

1. 用户登录/登出
   ↓
2. 调用AsyncManager.execute(AsyncFactory.recordLogininfor(...))
   ↓
3. AsyncFactory.recordLogininfor()
   - 解析User-Agent获取浏览器和操作系统
   - 获取IP地址
   - 根据IP查询登录地点
   - 创建SysLogininfor对象
   ↓
4. SysLogininforService.insertLogininfor()
   - 保存到数据库

4.3 参数处理流程

1. 检查是否需要保存请求参数(isSaveRequestData)
   ↓
2. 获取请求参数
   - GET请求:从request.getParameterMap()获取
   - POST/PUT请求:从方法参数获取
   ↓
3. 过滤敏感字段
   - 默认过滤:password、oldPassword、newPassword、confirmPassword
   - 注解指定过滤:excludeParamNames
   ↓
4. 过滤不需要的对象
   - MultipartFile、HttpServletRequest、HttpServletResponse、BindingResult
   ↓
5. 转换为JSON字符串(最大2000字符)
   ↓
6. 保存到operParam字段

五、使用指南

5.1 基本使用

在Controller方法上添加@Log注解:

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

    @Log(title = "用户管理", businessType = BusinessType.INSERT)
    @PostMapping
    public AjaxResult add(@RequestBody SysUser user) {
        // 业务逻辑
        return AjaxResult.success();
    }

    @Log(title = "用户管理", businessType = BusinessType.UPDATE)
    @PutMapping
    public AjaxResult edit(@RequestBody SysUser user) {
        // 业务逻辑
        return AjaxResult.success();
    }

    @Log(title = "用户管理", businessType = BusinessType.DELETE)
    @DeleteMapping("/{userIds}")
    public AjaxResult remove(@PathVariable Long[] userIds) {
        // 业务逻辑
        return AjaxResult.success();
    }

    @Log(title = "用户管理", businessType = BusinessType.QUERY)
    @GetMapping("/list")
    public TableDataInfo list(SysUser user) {
        // 业务逻辑
        return getDataTable(list);
    }
}

5.2 高级用法

5.2.1 排除敏感参数

@Log(
    title = "用户管理", 
    businessType = BusinessType.UPDATE,
    excludeParamNames = {"password", "secretKey"}
)
@PutMapping
public AjaxResult updateUser(@RequestBody SysUser user) {
    // 业务逻辑
}

5.2.2 不保存请求参数

@Log(
    title = "用户管理", 
    businessType = BusinessType.QUERY,
    isSaveRequestData = false
)
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
    // 业务逻辑
}

5.2.3 不保存响应结果

@Log(
    title = "用户管理", 
    businessType = BusinessType.QUERY,
    isSaveResponseData = false
)
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
    // 业务逻辑
}

5.2.4 指定操作人类别

@Log(
    title = "移动端用户管理", 
    businessType = BusinessType.INSERT,
    operatorType = OperatorType.MOBILE
)
@PostMapping
public AjaxResult add(@RequestBody SysUser user) {
    // 业务逻辑
}

5.3 业务类型枚举

public enum BusinessType {
    OTHER,      // 其它
    INSERT,     // 新增
    UPDATE,     // 修改
    DELETE,     // 删除
    GRANT,      // 授权
    EXPORT,     // 导出
    IMPORT,     // 导入
    FORCE,      // 强退
    GENCODE,    // 生成代码
    CLEAN,      // 清空数据
    QUERY       // 查询
}

5.4 操作人类别枚举

public enum OperatorType {
    OTHER,      // 其它
    MANAGE,     // 后台用户
    MOBILE      // 手机端用户
}

六、技术要点

6.1 AOP切面技术

  • 使用@Aspect@Component声明切面
  • 使用@Before@AfterReturning@AfterThrowing定义切点
  • 切点表达式:@annotation(controllerLog),拦截带@Log注解的方法

6.2 ThreadLocal使用

  • 使用NamedThreadLocal记录方法开始时间
  • 线程安全,避免并发问题
  • finally中清理,防止内存泄漏

6.3 异步处理

  • 使用ScheduledExecutorService实现异步
  • 延迟10ms执行,不阻塞主流程
  • 单例模式管理异步任务

6.4 敏感信息过滤

  • 默认过滤密码相关字段
  • 支持注解指定过滤字段
  • 使用PropertyPreExcludeFilter实现JSON序列化过滤

6.5 IP地址解析

  • 使用IpUtils.getIpAddr()获取真实IP
  • 使用AddressUtils.getRealAddressByIP()查询地理位置
  • 支持代理服务器场景

6.6 参数长度限制

  • 请求参数:最大2000字符
  • 响应结果:最大2000字符
  • 错误消息:最大2000字符
  • 使用StringUtils.substring()截断

6.7 异常处理

  • 切面捕获异常,不影响业务
  • 记录异常信息到日志
  • 异常时设置statusFAIL,记录errorMsg

七、扩展说明

7.1 ControllerLogsAspect(调试日志切面)

除了LogAspect,还有一个ControllerLogsAspect用于开发调试:

@Component
@Aspect
@Order(1)
@Slf4j
public class ControllerLogsAspect {

    @Pointcut("execution(public * com.construct.*.controller..*.*(..))")
    public void pointCut() {}

    @Before("pointCut()")
    public void doBeforeInServiceLayer(JoinPoint joinPoint) {
        // 记录开始时间
    }

    @AfterReturning(pointcut = "pointCut()", returning = "returnValue")
    public void log(JoinPoint joinPoint, Object returnValue) {
        // 打印调试日志
    }
}

功能:

  • 拦截所有Controller方法
  • 记录请求URL、参数、响应结果、处理时间
  • 仅非生产环境输出日志
  • 排除静态资源请求

7.2 业务日志扩展

系统还提供了业务日志扩展(如PubOperateLogDmsOperateLog),用于记录业务数据变更:特点:

  • 记录字段级别的变更(修改前/修改后)
  • 支持业务ID关联
  • 支持操作类型(创建/编辑)
  • 支持操作详情记录

7.3 日志查询和管理

系统提供了日志查询和管理功能:

  • 操作日志查询:/monitor/operlog/list
  • 登录日志查询:/monitor/logininfor/list
  • 日志导出:支持Excel导出
  • 日志删除:支持批量删除和清空

八、总结

8.1 设计优势

  1. 非侵入:通过注解实现,不影响业务代码
  2. 异步处理:日志写入不阻塞主流程
  3. 灵活配置:支持多种配置选项
  4. 安全过滤:自动过滤敏感信息
  5. 异常处理:完善的异常处理机制
  6. 性能优化:延迟执行、线程池管理

8.2 适用场景

  • 操作审计:记录用户操作
  • 问题排查:通过日志定位问题
  • 性能监控:记录方法执行时间
  • 安全审计:记录登录和操作信息

8.3 注意事项

  1. 日志表会持续增长,需要定期清理
  2. 异步执行可能丢失日志(极端情况)
  3. 参数长度限制可能截断重要信息
  4. IP地址解析依赖外部服务,可能影响性能
  5. 生产环境建议关闭调试日志切面

附录:相关文件清单

核心文件

  • LogAspect.java - 操作日志切面
  • ControllerLogsAspect.java - 控制器日志切面(调试用)
  • Log.java - 日志注解定义
  • AsyncManager.java - 异步任务管理器
  • AsyncFactory.java - 异步工厂
  • ThreadPoolConfig.java - 线程池配置

实体类

  • SysOperLog.java - 操作日志实体
  • SysLogininfor.java - 登录日志实体

服务层

  • ISysOperLogService.java - 操作日志服务接口
  • SysOperLogServiceImpl.java - 操作日志服务实现
  • ISysLogininforService.java - 登录日志服务接口
  • SysLogininforServiceImpl.java - 登录日志服务实现

数据层

  • SysOperLogMapper.java - 操作日志Mapper接口
  • SysOperLogMapper.xml - 操作日志Mapper XML
  • SysLogininforMapper.java - 登录日志Mapper接口
  • SysLogininforMapper.xml - 登录日志Mapper XML

控制器

  • SysOperlogController.java - 操作日志管理控制器
  • SysLogininforController.java - 登录日志管理控制器