本文目录导读:

对于 Java 项目中的操作日志,很多人会直接采用 log.info("用户[{}]执行了[{}]操作", userId, action) 这种硬编码方式,这种做法在初期简单直接,但随着业务复杂度的提升,会变得非常混乱。
要规整操作日志流程,核心在于将业务逻辑与日志记录解耦,并利用设计模式和框架来统一管理日志的收集、处理、存储。
以下是 Java 操作日志流程的规整方案,分为几个层次:
第一层:理清核心概念(元数据规范)
在动手编码前,先统一日志的数据结构,一个规范的操作日志应包含以下核心字段:
// 核心操作日志实体
public class OperationLog {
private Long id;
private String traceId; // 链路追踪ID(用于关联整个请求)
private Long operatorId; // 操作人ID
private String operatorName; // 操作人姓名
private String type; // 操作类型(如:CREATE, UPDATE, DELETE, LOGIN)
private String module; // 所属模块(如:订单管理, 用户管理)
private String businessId; // 业务对象ID(如:订单ID, 用户ID)
private String content; // 操作内容摘要(如:修改了订单金额)
private String detail; // 变更详情(如:金额从100改为200)
private String result; // 操作结果(SUCCESS / FAIL)
private Integer duration; // 操作耗时(毫秒)
private String ip; // 客户端IP
private Date createTime; // 操作时间
}
第二层:选择实现模式(主流方案)
根据项目复杂度和团队技术栈,选择以下三种主流方案之一:
方案1:注解 + AOP(最推荐,适用于中型以上项目)
这是目前企业级应用中最规范、最主流的方式,利用 Spring AOP 拦截自定义注解,自动生成日志。
步骤:
-
创建自定义注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface OperationLog { String module(); // 模块名 String type(); // 操作类型 String description() default ""; // 操作描述(支持SpEL) } -
定义切面类(Aspect):
@Aspect @Component public class OperationLogAspect { @Autowired private OperationLogService logService; // 持久化服务 @Around("@annotation(operationLog)") public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable { long startTime = System.currentTimeMillis(); Object result = null; boolean success = true; try { result = joinPoint.proceed(); } catch (Exception e) { success = false; throw e; } finally { long endTime = System.currentTimeMillis(); // 收集日志信息(这里需要解析参数、获取用户上下文) OperationLog log = buildLog(joinPoint, operationLog, success, endTime - startTime); // 异步写入数据库或消息队列(非常重要) logService.saveAsync(log); } return result; } } -
SpEL 解析: 为了动态获取方法中的参数(如订单ID),可以在注解中使用 Spring Expression Language (SpEL):
// 注解中:@OperationLog(description = "修改了订单: #{order.id}") // 切面中:使用 Spring 的 ExpressionParser 解析 #{order.id}
方案2:策略模式 + 函数式接口(适用于业务操作差异大)
当不同业务的日志生成逻辑完全不一样,无法通过通用模板解决时。
- 定义一个
LogRecordService接口:void record(LogRecord logRecord); - 针对每个业务场景(如订单创建、订单取消)实现不同的
LogRecordStrategy。 - 在业务代码中调用策略方法,而不是直接写 log。
缺点: 依然需要业务方手动调用,未完全解耦。
方案3:基于事件驱动(适用于分布式或微服务)
利用 Spring 的 ApplicationEvent。
- 定义事件:
class OperationLogEvent extends ApplicationEvent { // 包含日志数据 } - 发布事件: 在业务代码中
applicationEventPublisher.publishEvent(new OperationLogEvent(data)); - 监听事件:
@EventListener或@Async @EventListener异步处理日志写入。
第三层:处理棘手的“变更详情”
这是操作日志中最复杂的部分,即记录修改了哪些字段(diff)。
规整做法:
-
使用 JSON 序列化对比快照:
- 在更新接口中,先从数据库取出旧数据
oldObj。 - 接口接收新数据
newObj。 - 使用
Jackson或Gson将两者转为Map。 - 遍历
Map,对比值差异,只记录发生变化的字段。
- 在更新接口中,先从数据库取出旧数据
-
编写通用的 Diff 工具(避免重复代码):
public Map<String, Map<String, Object>> getDiff(Object oldObj, Object newObj, String... ignoreFields) { // 忽略掉 id, updateTime 等字段 // 返回值: {"price": {"old": 100, "new": 200}} } -
结合前端显示: 数据库存储 JSON 字符串(如
{"price": {"old": 100, "new": 200}}),前端解析后展示为“价格从 100 改为 200”。
第四层:存储与性能优化
- 避免同步写入: 操作日志对系统主流程不能有丝毫影响,必须异步处理。
- 队列化: 将日志对象放入
BlockingQueue或使用@Async线程池批量写入。 - 使用专门的日志框架: 对于高并发系统,不要使用 MySQL 存储操作日志,它很快会变成慢查询,可以考虑:
- Elasticsearch: 最适合,支持全文搜索和复杂聚合(如按用户查询、按时间查询)。
- MongoDB: 适合文档型存储,无 Schema 约束。
- ClickHouse: 适合超大规模的日志分析。
- 传统日志文件 + 采集: 通过 Logstash / Fluentd 采集日志文件,汇入 ES。
规整流程的最终形态
一个规整后的 Java 操作日志流程应该是这样的:
- 业务层(Service): 只关心业务逻辑,写上
@OperationLog(module="order", type="UPDATE")注解。 - 切面层(Aspect): 拦截注解,自动获取用户上下文、方法参数、执行结果、耗时。
- 快照层(Diff): 如果是更新操作,自动获取旧数据,计算 diff。
- 异步层(Async): 将组装好的
OperationLog对象丢进线程池或消息队列。 - 存储层(ES/DB): 异步线程批量消费,写入 Elasticsearch 或数据库。
- 查询层(API): 提供 RESTful API 供前端查询,支持分页、模糊搜索。
推荐的开源工具(避免重复造轮子)
如果你不想从零实现,可以考虑以下成熟的 Java 库:
- LogRecord(美团开源的
mzt-log): 专门针对操作日志,使用 SpEL 表达式,支持 Diff,非常强大。 - Mogu(蘑菇街的
mogu-log): 类似方案。 - AOP + 自定义注解: 如果需要轻量级,自己实现完整的注解+切面+Diff工具类也就 200-300 行代码,能完全掌控。
建议路线: 初期用注解+AOP+同步写入(几十行代码),业务稳定后改为异步+ES。