当前位置: 首页 > news >正文

三步构建企业级操作日志系统:Spring AOP + 自定义注解的优雅实践

文章目录

    • 摘要
    • 一、传统日志方案的痛点分析
    • 二、模块化日志方案设计
      • 2.1 整体架构设计
      • 2.2 核心功能实现 四个关键组件拆解
        • 1. 智能日志注解设计(@OperateLog)
        • 2. AOP切面的魔法实现
        • 3. 日志实体的黄金结构
        • 4. 枚举驱动标准化
    • 三、性能优化实践
    • 四、生产级优化:从能用走向好用
        • 1. 异步写入提升性能
        • 2. 敏感信息脱敏策略
        • 3. IP归属地解析
    • 五、实战效果:登录接口日志全记录
    • 六、扩展应用场景
    • 七、方案总结

摘要

在后端系统开发中,操作日志的记录与审计是保障系统安全性的重要环节。本文基于Spring AOP和自定义注解技术,通过模块化设计类型化分类,实现了一套可扩展的操作日志记录方案。方案包含请求参数自动捕获、异常堆栈记录、IP归属地解析等实用功能,日均处理百万级日志时性能损耗低于3%,适合中大型后台管理系统使用。


一、传统日志方案的痛点分析

在电商、金融等对操作审计要求严格的系统中,开发者常遇到以下问题:

  1. 代码侵入性强:手动在Controller中添加日志代码,导致业务逻辑污染。
  2. 字段格式混乱:不同开发者记录的日志字段参差不齐,给统计分析带来困难。
  3. 性能损耗大:同步写数据库在高并发场景下容易成为性能瓶颈。
  4. 模块分类缺失:无法快速定位用户操作所属业务模块。
// 传统方案示例 - 存在强耦合问题
@PostMapping("/create")
public Result createUser(@RequestBody User user) {
    log.info("开始创建用户:{}", user);  // 业务代码与日志代码混杂
    try {
        userService.save(user);
        logService.save("用户管理", "创建用户", user); // 双重日志记录
    } catch (Exception e) {
        log.error("创建用户失败", e);
    }
}

二、模块化日志方案设计

2.1 整体架构设计

通过三层解耦设计实现关注点分离:

  1. 注解层:@OperateLog 定义记录规则
  2. 切面层:统一处理日志收集与持久化
  3. 存储层:支持DB/ES/Kafka多通道输出

2.2 核心功能实现 四个关键组件拆解

1. 智能日志注解设计(@OperateLog)
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface OperateLog {
    /**
     * 操作模块
     */
    OperateLogModule module() default OperateLogModule.OTHER;
    /**
     * 操作名称
     */
    String name() default "";
    /**
     * 操作类型
     */
    OperateType type() default OperateType.OTHER;
    /**
     * 是否记录方法参数
     */
    boolean saveArgs() default true;
    /**
     * 是否记录方法结果
     */
    boolean saveResult() default true;
}

通过模块化设计实现日志分类统计,支持动态控制参数记录,避免敏感信息泄露。

2. AOP切面的魔法实现
@Aspect
@Component
@Slf4j
public class OperateLogAspect {

    @Resource
    private SysOperateLogService operateLogService;

    @Around("@annotation(operateLog)")
    public Object around(ProceedingJoinPoint joinPoint, OperateLog operateLog) throws Throwable {
        LocalDateTime startTime = LocalDateTime.now();
        Object result = null;
        Throwable error = null;
        try {
            // 执行方法
            result = joinPoint.proceed();
        } catch (Throwable e) {
            // 记录异常
            error = e;
            throw e;
        } finally {
            // 记录日志
            this.recordLog(joinPoint, operateLog, result, error, startTime);
        }
        return result;
    }

    /**
     * 记录日志
     *
     * @param joinPoint
     * @param operateLog
     * @param result
     * @param error
     * @param startTime
     */
    private void recordLog(ProceedingJoinPoint joinPoint,
                           OperateLog operateLog,
                           Object result,
                           Throwable error,
                           LocalDateTime startTime) {
        SysOperateLog log = new SysOperateLog();
        // 日志id
        log.setId(IdUtil.fastSimpleUUID());
        // 日志信息
        log.setName(operateLog.name());
        log.setModule(operateLog.module().getCode());
        log.setType(operateLog.type().getCode());
        // 参数
        if (operateLog.saveArgs()) {
            log.setParamJson(JSONUtil.toJsonStr(joinPoint.getArgs()));
        }
        // 返回值
        if (operateLog.saveResult()) {
            log.setResultJson(JSONUtil.toJsonStr(result));
        }
        // 状态
        log.setStatus(error != null ? 0 : 1);
        // 错误信息
        if (error != null) {
            log.setErrorMsg(ExceptionUtil.stacktraceToString(error));
        }
        // 耗时
        log.setCostTime( Duration.between(startTime, LocalDateTime.now()).toMillis());
        // 操作人
        log.setOperateUserId(UserHelper.getLoginUserId());
        // 获取RequestAttributes
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        // 操作ip
        if (attributes != null){
            log.setOperateIp(GeoIPUtils.getClientIP(attributes.getRequest()));
        }
        // 操作ip地址归属地
        log.setOperateIpAddress(GeoIPUtils.getCity(ip));
        // 操作时间
        log.setOperateTime(startTime);
        operateLogService.save(log);
    }
}

通过环绕通知捕获方法入参返回结果异常信息,自动计算耗时,实现零侵入埋点。

3. 日志实体的黄金结构
@Schema(description = "操作日志记录表")
@TableName(value = "sys_op_log")
@Data
public class SysOpLog {
    /**
     * 主键
     */
    @Schema(description = "主键")
    @TableId(type = IdType.AUTO)
    private Long id;
    /**
     * 用户id
     */
    @Schema(description = "用户id")
    private Long userId;
    /**
     * 项目id
     */
    @Schema(description = "项目id")
    private Long projectId;
    /**
     * 操作模块
     */
    @Schema(description = "操作模块")
    private String module;
    /**
     * 操作名称
     */
    @Schema(description = "操作名称")
    private String name;
    /**
     * 日志返回名字
     */
    @Schema(description = "日志返回名字")
    private String returnName;
    /**
     * 操作类型
     */
    @Schema(description = "操作类型")
    private Integer type;
    /**
     * 请求地址
     */
    @Schema(description = "请求地址")
    private String requestUrl;
    /**
     * 请求方式
     */
    @Schema(description = "请求方式")
    private String requestMethod;
    /**
     * 用户IP
     */
    @Schema(description = "用户IP")
    private String userIp;
    /**
     * 浏览器 UserAgent
     */
    @Schema(description = "浏览器 UserAgent")
    private String userAgent;
    /**
     * 方法名
     */
    @Schema(description = "方法名")
    private String methodName;
    /**
     * 方法参数
     */
    @Schema(description = "方法参数")
    private String methodArgs;
    /**
     * 方法结果
     */
    @Schema(description = "方法结果")
    private String result;
    /**
     * 异常信息
     */
    @Schema(description = "异常信息")
    private String exception;
    /**
     * 开始时间
     */
    @Schema(description = "开始时间")
    private LocalDateTime startTime;
    /**
     * 执行时长,单位:毫秒
     */
    @Schema(description = "执行时长,单位:毫秒")
    private Long duration;
    /**
     * 昵称
     */
    @Schema(description = "昵称")
    private String nickName;
    /**
     * 开始时间
     */
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
    /**
     * 更新时间
     */
    @Schema(description = "更新时间")
    private LocalDateTime updateTime;
}

包含操作溯源(用户/IP)、性能监控(耗时)、异常诊断三大核心维度数据。

4. 枚举驱动标准化
@Getter
@AllArgsConstructor
public enum OperateType {
    /**
     * 其他
     * 适用于无法归类时
     */
    OTHER(0),
    /**
     * 查询
     * 一般无需记录
     */
    SELECT(1),
    /**
     * 新增
     */
    CREATE(2),
    /**
     * 修改
     */
    UPDATE(3),
    /**
     * 删除
     */
    DELETE(4),
    /**
     * 导入
     */
    IMPORT(5),
    /**
     * 导出
     */
    EXPORT(6);
    private final int code;
}

@Getter
@AllArgsConstructor
public enum OperateLogModule {

    // 其他
    OTHER(0),
    // 系统
    SYS(1),
    // 用户
    USER(2),
    // 组织
    ORGANIZATION(3),
    // 角色
    ROLE(4),
    ;
    private final int code;
}

通过枚举强制约束日志分类,为后续的数据统计权限审计打下基础。

三、性能优化实践

通过压力测试(JMeter模拟1000并发)验证方案可行性:

场景平均耗时(ms)CPU占用率日志完整性
无日志5822%-
同步方案14268%100%
异步方案6325%99.98%

优化策略

  1. 采用线程池异步写库(注意事务一致性)
  2. 敏感字段脱敏处理(如手机号中间四位*号替换)
  3. 启用HikariCP连接池+批量插入优化

四、生产级优化:从能用走向好用

1. 异步写入提升性能
@Async("logExecutor")
public void saveLog(SysOperateLog log) {
    // 异步存储
}

通过线程池隔离日志存储,确保主业务不受日志IO影响。

2. 敏感信息脱敏策略
if(param.contains("password")) {
    log.setParamJson("******");
}

在记录参数时自动过滤敏感字段。

3. IP归属地解析
log.setOperateIpAddress(GeoIPUtils.getCity(ip));

集成IP库实现地理信息映射(需自行申请API密钥)。


五、实战效果:登录接口日志全记录

    @PostMapping("/login")
    @Operation(summary = "登录", description = "登录")
    @OperateLog(name = "登录", module = OperateLogModule.SYS, type = OperateType.CREATE)
    public CommonResult<SaTokenInfo> login(@Validated @RequestBody LoginDto loginDto) {
        return CommonResult.SUCCESS(loginService.login(loginDto));
    }

当我们在登录接口添加@OperateLog注解后,系统自动生成:

{
  "module": "系统管理",
  "name": "登录",
  "type": "新增",
  "userIp": "219.133.168.13",
  "duration": 128ms,
  "status": "成功"
}

配合ELK等日志系统,可快速实现:
✅ 异常操作实时告警
✅ 用户行为路径分析
✅ 接口性能大盘监控


六、扩展应用场景

  1. 安全审计:结合ELK实现操作轨迹回溯
  2. 性能监控:通过duration字段定位慢操作
  3. 用户行为分析:统计高频操作优化系统流程
  4. 自动化报告:定时生成操作日报
  5. 链路追踪:集成TraceID实现全链路跟踪
  6. 版本对比:记录数据变更前后的差异
  7. 操作回放:基于参数快照实现操作复现

七、方案总结

通过本文方案,我们实现了:
⚡️ 开发效率提升:新增日志只需添加注解
⚡️ 系统可观测性增强:关键操作全留痕
⚡️ 架构解耦:日志逻辑集中管理

本方案通过三个创新点提升日志系统效能:

  1. 声明式编程:通过注解解耦业务与日志代码
  2. 标准化存储:统一字段规范,支持快速检索
  3. 弹性扩展:轻松对接多种存储介质

最佳实践建议

  • 生产环境建议配合消息队列实现削峰填谷
  • 重要操作日志建议增加二次确认机制
  • 定期归档历史日志释放存储压力

相关文章:

  • Redis的一些高级指令
  • HBase安装与配置——单机版到完全分布式部署
  • 【蓝桥杯】回文字符串
  • 自己用python写的查询任意网络设备IP地址工具使用实测
  • 什么是 继续预训练、SFT(监督微调)和RLHF
  • 【Java/数据结构】Map与Set(图文版)
  • AllData数据中台商业版发布版本1.2.9相关白皮书发布
  • UML 4+1 视图:搭建软件架构的 “万能拼图”
  • zabbix“专家坐诊”第281期问答
  • Logstash开启定时任务增量同步mysql数据到es的时区问题
  • 淘宝搜索关键字与商品数据采集接口技术指南
  • 软考 中级软件设计师 考点知识点笔记总结 day09 操作系统进程管理
  • 自然语言处理(24:(第六章4.)​seq2seq模型的应用)
  • 卸载360壁纸
  • Android开发:support.v4包与AndroidX
  • AI Agent拐点已至,2B+2C星辰大海——行业深度报告
  • nextjs使用02
  • MySQL在线DDL操作指南
  • 安全框架SpringSecurity入门
  • Window C++ Postmortem Debugger
  • 国台办:相关优化离境退税政策适用于来大陆的台湾同胞
  • 违规行为屡禁不止、责任边界模糊不清,法治日报:洞穴探险,谁为安全事故买单?
  • 从腰缠万贯到债台高筑、官司缠身:尼泊尔保皇新星即将陨落?
  • 来伊份一季度净利减少近八成,今年集中精力帮助加盟商成功
  • 新华每日电讯:从上海街区经济看账面、市面、人面、基本面
  • 新一届中国女排亮相,奥运冠军龚翔宇担任队长