1. 概述
common-audit-log 是一个基于 AOP 的审计日志组件,支持操作日志的自动收集、差异比对和存储。该组件通过注解方式轻松集成到 Spring Boot 应用中,实现操作日志的自动化记录。
2. 核心特性
- 注解驱动:通过简单的注解即可实现日志记录
- SpEL 表达式支持:动态填充日志操作描述,操作分类,操作数据id等信息
- 差异比对:支持操作前后数据差异自动比对
- 支持多层级嵌套调用日志:支持多层级嵌套调用日志
- 条件性记录:支持根据条件决定是否记录日志
- 异步收集:支持异步日志收集,不影响主业务性能
- 配套查询系统(可选): 支持多维条件查询日志,开箱即用
3. 模块说明
| 模块 | 说明 |
|---|---|
| common-audit-log-core | 核心功能模块,包含注解、AOP 切面、差异比对等 |
| common-audit-log-runtime | 运行时配置模块,包含自动配置和属性配置 |
| common-audit-log-web | 日志查询配套系统和接入示例 |
4. 快速开始
4.1 添加依赖
<dependency>
<groupId>com.taoyuanx</groupId>
<artifactId>common-audit-log-core</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.taoyuanx</groupId>
<artifactId>common-audit-log-runtime</artifactId>
<version>1.0.0</version>
</dependency>
4.2 配置文件
在 application.yml 中添加配置:
audit:
log:
# 是否异步处理日志
async: true
# 异步队列大小
log-queue-size: 1000
# 异步收集线程收集间隔(毫秒)
collect-interval: 50
# 收集队列满时的等待时间(毫秒),负数则入队失败丢弃
queue-full-wait-time: 2000
4.3 启用自动配置
确保 Spring Boot 启动类扫描到组件包:
@SpringBootApplication
@ComponentScan(basePackages = {"com.taoyuanx.common.audit.log"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
5. 核心注解
5.1 @OperateLog
标准的操作日志注解,用于标记需要记录日志的方法。
属性说明:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| bizType | String | 是 | 业务类型,如"用户管理"、"订单管理" |
| subBizType | String | 否 | 子业务类型 |
| operateObject | String | 否 | 操作对象,支持 SpEL 表达式 |
| success | String | 是 | 操作成功时的日志描述模板,支持 SpEL 表达式 |
| fail | String | 否 | 操作失败时的日志描述模板,支持 SpEL 表达式 |
| condition | String | 否 | 记录条件,支持 SpEL 表达式,true 才记录 |
| ignoreException | Class[] | 否 | 忽略的异常类型 |
使用示例:
@PostMapping("/user/add")
@ResponseBody
@OperateLog(
success = "'新增了用户:' + #result.get('username')",
fail = "'新增用户失败:' + #error.message",
bizType = "用户管理",
operateObject = "#result.get('username')"
)
public Map addUser(@RequestBody Map<String, Object> params) {
// 业务逻辑
return params;
}
5.2 @SimpleOperateLog
简化版操作日志注解,适用于简单场景。
属性说明:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| bizType | String | 是 | 业务类型 |
| subBizType | String | 否 | 子业务类型 |
| operateObject | String | 否 | 操作对象 |
| operateDesc | String | 是 | 操作描述,支持 SpEL 表达式 |
使用示例:
@PostMapping("/user/add")
@ResponseBody
@SimpleOperateLog(
operateDesc = "'新增了用户:' + #params.get('username')",
bizType = "用户管理",
operateObject = "#params.get('username')"
)
public Map addUser(@RequestBody Map<String, Object> params) {
// 业务逻辑
return params;
}
5.3 @LogDiff
数据差异比对注解,配合 @OperateLog 使用,用于记录数据变更前后对比。
属性说明:
| 属性 | 类型 | 必填 | 说明 |
|---|---|---|---|
| before | String | 否 | 操作前数据获取表达式 |
| after | String | 否 | 操作后数据获取表达式 |
| diffHandler | Class | 否 | 对象对比器,默认 NoDiffHandler |
使用示例:
// 新增操作(只有操作后数据)
@PostMapping("/user/add")
@ResponseBody
@OperateLog(
success = "'新增了用户:' + #result.get('username')",
bizType = "用户管理",
operateObject = "#result.get('username')"
)
@LogDiff(after = "#result")
public Map addUser(@RequestBody Map<String, Object> params) {
// 业务逻辑
return result;
}
// 删除操作(只有操作前数据)
@PostMapping("/user/delete")
@ResponseBody
@OperateLog(
success = "'删除了用户:' + #params.get('userId')",
bizType = "用户管理"
)
@LogDiff(before = "#result.get('deletedUser')")
public Map deleteUser(@RequestBody Map<String, Object> params) {
// 业务逻辑
return result;
}
// 更新操作(有前后数据)
@PostMapping("/user/update")
@ResponseBody
@OperateLog(
success = "'更新了用户:' + #result.get('username')",
bizType = "用户管理"
)
public Map updateUser(@RequestBody Map<String, Object> params) {
// 通过上下文设置前后数据
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_BEFORE_OBJECT, beforeUser);
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_AFTER_OBJECT, afterUser);
return result;
}
6. SpEL 表达式支持
组件支持 Spring SpEL 表达式,常用变量如下:
| 变量 | 说明 | 示例 |
|---|---|---|
#params | 方法参数 | #params.get('username') |
#result | 方法返回值 | #result.get('id') |
#error | 异常对象 | #error.message |
#operateObject | 操作对象 | #operateObject |
示例:
@OperateLog(
success = "'操作人:' + #params.get('operator') + ' 处理了订单:' + #result.get('orderId')",
bizType = "订单管理",
operateObject = "#result.get('orderId')"
)
7. 上下文工具类
AuditLogContextUtil 提供上下文管理功能,用于在代码中设置自定义信息。
7.1 常用常量
| 常量 | 说明 |
|---|---|
| CONTEXT_KEY_OPERATOR | 操作人 |
| CONTEXT_KEY_TENANT | 租户 |
| CONTEXT_KEY_BEFORE_OBJECT | 操作前对象 |
| CONTEXT_KEY_AFTER_OBJECT | 操作后对象 |
| CONTEXT_KEY_OPERATE_DSL | 操作 DSL |
| CONTEXT_KEY_TRACE_ID | 追踪 ID |
| CONTEXT_KEY_EXT | 扩展信息 |
7.2 常用方法
// 设置操作人
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_OPERATOR, "admin");
// 设置租户
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_TENANT, "tenant1");
// 设置操作前后对象(用于差异比对)
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_BEFORE_OBJECT, beforeData);
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_AFTER_OBJECT, afterData);
// 设置自定义操作详情
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_OPERATE_DSL, customDetail);
// 设置扩展信息
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_EXT, extInfo);
// 设置追踪 ID
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_TRACE_ID, traceId);
// 清理上下文
AuditLogContextUtil.remove();
8. 使用场景示例
8.1 简单日志记录
@PostMapping("/simple/log")
@ResponseBody
@OperateLog(
success = "'新增了:' + #params.get('flowBizNo') + ',操作结果:' + #result.get('flowBizNo')",
bizType = "测试",
operateObject = "#params.get('flowBizNo')"
)
public Map logSimple(@RequestBody Map<String, Object> params) {
return params;
}
8.2 带差异比对的新增操作
@PostMapping("/logWithDetail")
@ResponseBody
@OperateLog(
success = "'新增了:' + #params.get('flowBizNo')",
bizType = "测试",
operateObject = "#result.get('addObject').get('name')"
)
@LogDiff(after = "#result.get('addObject')")
public Map logWithDetail(@RequestBody Map<String, Object> params) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> addObject = new HashMap<>();
addObject.put("name", "张三");
addObject.put("age", 25);
result.put("addObject", addObject);
return result;
}
8.3 带差异比对的删除操作
@PostMapping("/logWithDelete")
@ResponseBody
@OperateLog(
success = "'删除了:' + #params.get('flowBizNo')",
bizType = "测试",
operateObject = "#result.get('deleteObject').get('name')"
)
@LogDiff(before = "#result.get('deleteObject')")
public Map logWithDelete(@RequestBody Map<String, Object> params) {
Map<String, Object> result = new HashMap<>();
Map<String, Object> deleteObject = new HashMap<>();
deleteObject.put("name", "李四");
deleteObject.put("age", 30);
result.put("deleteObject", deleteObject);
return result;
}
8.4 带差异比对的更新操作
@PostMapping("/logWithUpdate")
@ResponseBody
@OperateLog(
success = "'更新了:' + #params.get('flowBizNo')",
bizType = "测试",
operateObject = "#result.get('name')"
)
public Map logWithUpdate(@RequestBody Map<String, Object> params) {
// 设置操作前数据
Map<String, Object> before = new HashMap<>();
before.put("name", "王五");
before.put("age", 35);
// 设置操作后数据
Map<String, Object> after = new HashMap<>();
after.put("name", "王五");
after.put("age", 36);
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_BEFORE_OBJECT, before);
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_AFTER_OBJECT, after);
return params;
}
8.5 自定义日志详情
@PostMapping("/logWithCustomDetail")
@ResponseBody
@OperateLog(
success = "'自定义日志详情:' + #params.get('flowBizNo')",
bizType = "测试",
operateObject = "#result.get('name')"
)
public Map logWithCustomDetail(@RequestBody Map<String, Object> params) {
Map<String, Object> detail = new HashMap<>();
detail.put("customField1", "value1");
detail.put("customField2", "value2");
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_OPERATE_DSL, detail);
return params;
}
8.6 带扩展信息的日志
@PostMapping("/logWithExt")
@ResponseBody
@OperateLog(
success = "'自定义日志详情:' + #params.get('flowBizNo')",
bizType = "测试",
operateObject = "#result.get('name')"
)
public Map logWithExt(@RequestBody Map<String, Object> params) {
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_OPERATE_DSL, params.get("detail"));
AuditLogContextUtil.set(AuditLogContextUtil.CONTEXT_KEY_EXT, params.get("ext"));
return params;
}
8.7 条件性日志记录
@PostMapping("/logConditional")
@ResponseBody
@OperateLog(
success = "'自定义日志详情:' + #params.get('flowBizNo')",
bizType = "测试",
operateObject = "#result.get('name')",
condition = "#result.get('condition') == true"
)
public Map logConditional(@RequestBody Map<String, Object> params) {
// 只有当 condition 为 true 时才会记录日志
return params;
}
8.8 嵌套线程日志
@GetMapping("/nestLogTest")
@ResponseBody
@OperateLog(
success = "'嵌套日志层级:' + #operateObject",
bizType = "嵌套日志测试",
operateObject = "#operateObject"
)
public Map nestLogTest(Long operateObject) {
// 调用其他服务方法,该方法的日志会嵌套在当前日志中
demoLogService.businessLog(2L);
return new HashMap();
}
9. 数据库配置
9.1 初始化脚本
项目提供了 MySQL、SQLite、H2 三种数据库的初始化脚本:
- MySQL:
src/main/resources/init/mysql/init.sql - SQLite:
src/main/resources/init/sqlite/init.sql - H2:
src/main/resources/init/h2/init.sql
9.2 配置示例
SQLite 配置:
spring:
datasource:
driver-class-name: org.sqlite.JDBC
url: jdbc:sqlite:./data/audit_log.db
MySQL 配置:
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8
username: root
password: root
H2 配置:
spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:./data/audit_log
username: sa
password:
10. 查询接口
10.1 分页查询日志
POST /api/audit/logs/page
Content-Type: application/json
{
"pageNum": 1,
"pageSize": 10,
"operator": "user1",
"bizType": "USER",
"startTime": 1704067200000,
"endTime": 1704153600000
}
10.2 查询日志详情
GET /api/audit/logs/detail?logId=1
10.3 日志查询页面
审计日志组件提供了配套的 Web 查询界面,访问 /log.html 可以使用可视化界面查询和查看日志详情。
查询条件功能
- 业务类型筛选:根据业务类型(如"用户管理"、"订单管理")进行筛选
- 业务子类型筛选:根据业务子类型进行更细粒度的筛选
- 操作人筛选:根据操作人账号进行筛选
- 租户筛选:根据租户信息进行筛选
- 操作结果筛选:支持选择"全部"、"成功"、"失败"
- 操作对象筛选:根据操作对象进行筛选
- 操作描述筛选:根据操作描述进行模糊查询
- 时间范围查询(必填):
- 开始时间和结束时间必须填写
- 支持快捷时间选择:
- 今天:00:00:00 ~ 23:59:59
- 昨天:00:00:00 ~ 23:59:59
- 前天:00:00:00 ~ 23:59:59
- 最近15分钟/30分钟/1小时/2小时/3小时/4小时/12小时/24小时
- 时间范围限制:查询时间范围不能超过24小时
日志列表展示
列表表格包含以下列:
- 操作人:执行操作的用户账号
- 业务类型:业务分类
- 操作类型:子业务类型
- 操作描述:操作的描述信息
- 操作对象:被操作的对象标识
- 操作结果:成功(绿色标签)或失败(红色标签)
- 租户:租户信息
- 操作时间:操作发生的时间
- Trace ID:链路追踪ID
- 异常信息:异常错误信息(如有)
- 扩展信息:自定义扩展信息
- 操作:查看详情按钮
日志详情查看
点击"查看详情"按钮,可查看日志的详细信息,包括:
- 基本信息:ID、操作人、业务类型、操作描述等
- 操作结果:成功/失败状态
- 操作时间、Trace ID、异常信息、扩展信息
- 耗时(ms):操作执行耗时
- 变更详情:支持 JSON 格式化展示和差异比对展示
11. 最佳实践
- 合理设置业务类型:使用规范的业务类型,便于日志分类和查询
- 使用 SpEL 表达式:充分利用 SpEL 表达式动态获取关键信息
- 差异比对:对于重要数据的变更,使用差异比对功能记录变更详情
- 异步收集:生产环境建议开启异步收集,避免影响主业务性能
- 条件记录:对于高频操作,可以使用条件表达式控制日志记录频率
- 扩展信息:使用扩展信息字段记录业务相关的自定义信息
12. 常见问题
Q: 日志没有记录?
A: 检查以下几点:
- 确保
@OperateLog注解的方法被 Spring 容器管理 - 检查
condition条件是否满足 - 检查是否有配置
ignoreException忽略了异常
Q: 如何自定义差异比对逻辑?
A: 实现 ObjectDiffHandler 接口,并在 @LogDiff 注解中指定:
@LogDiff(
before = "#before",
after = "#after",
diffHandler = CustomDiffHandler.class
)
Q: 如何追踪多线程操作的日志?
A: 使用 AuditLogContextUtil 的嵌套线程本地存储,组件会自动处理线程间上下文传递。