Java案例如何实现操作日志?

wen python案例 3

Java案例如何实现操作日志?5种实战方案深度解析

📖 目录导读

  1. 操作日志的核心价值 - 为什么业务系统必须记录操作日志?
  2. 五大实现方案对比 - AOP切面、数据库触发器、事件驱动、中间件、自定义注解
  3. 基于Spring AOP的通用日志切面(最推荐)
  4. MyBatis-Plus自动填充与日志入库
  5. RabbitMQ异步日志收集(高并发场景)
  6. 自定义注解+反射实现灵活标记
  7. 数据库触发器与存储过程(遗留系统适配)
  8. 常见问题与避坑指南(面试高频)
  9. 总结与最佳实践

❓ 问答预热

Q:操作日志和系统日志有什么区别?
A:操作日志记录“谁在什么时候对什么资源做了什么操作”,如“用户A于10:00删除了订单B”,关注业务语义,系统日志(Log4j等)记录技术异常、调试信息,两者不可混用。

Java案例如何实现操作日志?

Q:为什么不用简单的System.out.println
A:不满足生产环境要求:①无法统一管理②性能低③无持久化④不利于问题追溯,企业级日志必须满足结构化、可查询、异步化、低侵入四大特性。


基于Spring AOP的通用日志切面(最推荐)

适用场景

多数CRUD系统,要求日志与业务代码解耦,且对性能要求中等的场景。

核心思路

通过AOP环绕通知拦截Controller或Service方法,提取方法入参、注解、返回值,自动记录日志。

代码案例(关键步骤)

// 1. 定义自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogRecord {
    String value() default ""; // 日志描述
    boolean saveParam() default true; // 是否保存参数
}
// 2. AOP切面实现
@Aspect
@Component
public class LogAspect {
    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
    @Around("@annotation(logRecord)")
    public Object around(ProceedingJoinPoint joinPoint, LogRecord logRecord) throws Throwable {
        // 前置处理:获取请求信息
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        String desc = logRecord.value();
        // 执行目标方法
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long cost = System.currentTimeMillis() - start;
        // 后置处理:组装日志对象
        OperationLog operationLog = new OperationLog();
        operationLog.setDescription(desc);
        operationLog.setMethod(methodName);
        operationLog.setParams(logRecord.saveParam() ? JSON.toJSONString(args) : null);
        operationLog.setResult(result != null ? JSON.toJSONString(result) : null);
        operationLog.setCostTime(cost);
        operationLog.setCreateTime(new Date());
        // 异步保存日志(避免主业务阻塞)
        CompletableFuture.runAsync(() -> logService.save(operationLog));
        return result;
    }
}

健壮性优化

  • 参数脱敏:对密码、身份证等字段使用@Sensitive注解过滤
  • 失败补偿:若日志保存失败,打印告警而非影响主流程
  • 防重复记录:使用ThreadLocal标记是否为本切面触发的递归调用

MyBatis-Plus自动填充与日志入库

适用场景

使用MyBatis-Plus的项目,希望自动捕获所有数据库变更记录。

核心实现

通过MetaObjectHandler实现插入/更新时的自动填充,配合拦截器记录变更前后对比。

@Component
public class LogMetaHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        // 自动填充创建人、创建时间
        this.strictInsertFill(metaObject, "createBy", String.class, "当前用户");
    }
    @Override
    public void updateFill(MetaObject metaObject) {
        // 可在此记录修改前快照(需配合拦截器)
    }
}
// 配合Interceptor记录变更差异
@Intercepts({@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})})
public class DiffLogInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 读取修改前的数据
        // 2. 执行修改
        // 3. 对比修改前后字段差异
        // 4. 写入日志表
    }
}

注意:此方案不适合复杂业务逻辑,且需要自行实现字段对比算法。


RabbitMQ异步日志收集(高并发场景)

场景定位

日活百万以上、日志写入量大的系统,直接入库会压垮数据库。

架构设计

调用方 → 日志切面 → MQ生产者 (Direct Exchange) → 消费者 → 批量写入ES/MySQL

生产者端(关键配置)

// 异步发送,无需等待ACK
rabbitTemplate.convertAndSend("log.exchange", "log.routing", operationLog, 
    message -> {
        message.getMessageProperties().setDeliveryMode(MessageDeliveryMode.PERSISTENT);
        return message;
    });

消费者端优化

@RabbitListener(queues = "log.queue")
public void consume(List<OperationLog> logs) {
    // 批量操作:每100条或每5秒刷一次
    logService.saveBatch(logs);
}

可靠性保障

  • 配置重试机制(失败后重新入队)
  • 设置死信队列处理持久异常
  • 定期对账:对比MQ发送量与落库量

自定义注解+反射实现灵活标记

差异化需求

某些操作需要额外记录关联ID(如订单号),或某些方法不需要记录。

注解设计

@Target({ElementType.METHOD, ElementType.TYPE})
public @interface BusinessLog {
    String module(); // 模块名,如“订单”
    String operation(); // 操作类型,如“创建”
    String bindKey() default ""; // 关联业务ID的SpEL表达式,如 "#order.id"
}

解析SpEL

ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setVariable("args", joinPoint.getArgs());
String orderId = parser.parseExpression(annotation.bindKey()).getValue(context, String.class);

数据库触发器与存储过程(遗留系统适配)

技术特点

零代码改动,但耦合度高,难以维护。

-- 简单示例
CREATE TRIGGER trg_after_update_order
AFTER UPDATE ON `order`
FOR EACH ROW
BEGIN
    INSERT INTO op_log (table_name, old_data, new_data, operator, op_time)
    VALUES ('order', OLD.content, NEW.content, @current_user, NOW());
END;

使用限制

  • 跨数据库迁移困难
  • 无法记录HTTP上下文信息(IP、请求路径等)
  • 多次触发可能导致性能问题

🛠️ 常见问题与避坑指南

问题1:AOP切面导致循环依赖

解决:使用@Lazy注解或Setter注入,避免构造器循环依赖。

问题2:异步日志导致事务不一致

场景:主业务回滚,但日志已写入。
方案:采用事务同步策略,在TransactionSynchronizationManagerafterCommit中发送MQ。

问题3:日志数据量过大

优化

  • 区分核心日志与辅助日志,核心日志存MySQL,辅助日志入Elasticsearch
  • TTL索引:设置日志表保留90天数据,定期归档到冷存储

问题4:敏感信息泄露

必须处理

  • 参数序列化前替换敏感字段(如password → )
  • 使用Jackson@JsonFilter进行属性过滤

📌 总结与最佳实践

方案选择决策树

日志写入量 < 10万/天 → 方案一(AOP + 同步入库)
10万~100万/天 → 方案三(MQ异步) + 方案二(MyBatis拦截器)
> 100万/天 → 方案三 + Elasticsearch集群
遗留系统 → 方案五(触发器过渡)
需要精细控制 → 方案四(自定义注解)

最终推荐组合

  • 核心日志:Spring AOP + 自定义注解 + 异步写入MySQL
  • 变更历史:MyBatis-Plus拦截器 + 快照对比
  • 审计报表:日志数据同步到Elasticsearch,使用Kibana可视化

验收标准

  1. 日志与业务代码完全解耦(修改日志逻辑无需改业务代码)
  2. 单次日志写入对主业务响应时间影响 < 2ms
  3. 日志完整率 > 99.99%(需做幂等和补偿)
  4. 支持按模块、用户、时间段、操作类型组合检索

延伸思考:若使用Spring Cloud微服务架构,可集成Spring Cloud Sleuth,在日志中注入traceId,实现全链路追踪,此时日志表建议增加trace_id字段,配合Zipkin进行调用链分析。

抱歉,评论功能暂时关闭!