一、数据库表结构设计与初始化
实现审计跟踪的第一步是建立能够存储详细操作日志的数据表。该表需要记录操作人、时间、IP、模块、操作类型、具体参数以及执行结果。以下是基于MySQL的完整建表语句,包含所有必要字段,直接执行即可。
```sql
CREATE TABLE `sys_audit_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`module_name` varchar(50) NOT NULL COMMENT '模块名称',
`action_name` varchar(50) NOT NULL COMMENT '操作名称',
`method_name` varchar(100) NOT NULL COMMENT '执行方法名',
`user_id` bigint(20) DEFAULT NULL COMMENT '操作人ID',
`username` varchar(50) DEFAULT NULL COMMENT '操作人账号',
`ip_address` varchar(50) DEFAULT NULL COMMENT '请求IP地址',
`request_params` longtext COMMENT '请求参数',
`response_result` longtext COMMENT '返回结果',
`status` tinyint(2) DEFAULT '1' COMMENT '状态:1成功,0失败',
`error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
`execute_time` bigint(20) DEFAULT NULL COMMENT '执行时长(毫秒)',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统审计跟踪日志表';
```
二、项目核心依赖配置
本指南基于Spring Boot 2.7.x版本,使用AOP进行切面日志采集,使用MyBatis-Plus进行数据持久化。请确保在pom.xml中引入以下依赖。不要遗漏fastjson,它用于参数和结果的JSON序列化。
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-aop
com.baomidou
mybatis-plus-boot-starter
3.5.2
mysql
mysql-connector-java
runtime
com.alibaba
fastjson
1.2.83
org.projectlombok
lombok
true
```
三、自定义审计注解定义
为了方便在业务代码中标记需要审计的方法,我们需要定义一个自定义注解@AuditLog。通过该注解,开发者可以声明当前操作的模块名称和具体动作,例如“档案管理-上传文件”。
创建包com.example.archive.annotation,并写入以下代码:
```java
package com.example.archive.annotation;
import java.lang.annotation.;
@Target(ElementType.METHOD) // 作用在方法上
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
@Documented
public @interface AuditLog {
/
模块名称,如:档案管理
/
String module() default "";
/
操作名称,如:新增档案
/
String action() default "";
}
```
四、AOP切面核心逻辑实现
这是审计功能的核心。切面会拦截所有带有@AuditLog注解的方法,捕获方法执行前后的参数、返回值、异常以及执行耗时。请确保IP获取工具类正确配置,否则记录的IP将为空。
创建包com.example.archive.aspect,创建AuditLogAspect类:
```java
package com.example.archive.aspect;
import com.alibaba.fastjson.JSON;
import com.example.archive.annotation.AuditLog;
import com.example.common.util.IpUtil; // 假设你有IP工具类,若没有需自行实现获取Request逻辑
import com.example.entity.SysAuditLog;
import com.example.service.SysAuditLogService;
import org.aspectj.lang.ProceedingJoinPoint;
org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder; // 假设使用Spring Security
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Date;
@Aspect
@Component
public class AuditLogAspect {
@Autowired
private SysAuditLogService auditLogService;
@Around("@annotation(auditLog)")
public Object around(ProceedingJoinPoint point, AuditLog auditLog) throws Throwable {
long beginTime = System.currentTimeMillis();
Object result = null;
Exception exception = null;
// 1. 执行业务逻辑
try {
result = point.proceed();
return result;
} catch (Exception e) {
exception = e;
throw e; // 异常必须抛出,否则事务无法回滚
} finally {
long endTime = System.currentTimeMillis();
saveLog(point, auditLog, result, exception, endTime - beginTime);
}
}
private void saveLog(ProceedingJoinPoint point, AuditLog auditLog, Object result, Exception exception, long time) {
MethodSignature signature = (MethodSignature) point.getSignature();
Method method = signature.getMethod();
SysAuditLog logEntity = new SysAuditLog();
// 设置注解上的模块和动作
logEntity.setModuleName(auditLog.module());
logEntity.setActionName(auditLog.action());
logEntity.setMethodName(method.getDeclaringClass().getName() + "." + method.getName());
// 获取请求参数
Object[] args = point.getArgs();
try {
String params = JSON.toJSONString(args);
// 防止参数过长导致数据库存储失败,可根据实际情况截断
if (params.length() > 2000) {
params = params.substring(0, 2000);
}
logEntity.setRequestParams(params);
} catch (Exception e) {
logEntity.setRequestParams("参数解析异常");
}
// 设置返回结果
if (result != null) {
try {
String response = JSON.toJSONString(result);
if (response.length() > 2000) {
response = response.substring(0, 2000);
}
logEntity.setResponseResult(response);
} catch (Exception e) {
logEntity.setResponseResult("结果解析异常");
}
}
// 设置状态与异常信息
if (exception != null) {
logEntity.setStatus(0);
logEntity.setErrorMsg(exception.getMessage());
} else {
logEntity.setStatus(1);
}
logEntity.setExecuteTime(time);
// 获取当前用户信息 (需根据你的项目认证体系调整,此处为示例)
try {
// String username = SecurityContextHolder.getContext().getAuthentication().getName();
// logEntity.setUsername(username);
// 临时模拟,实际请替换为获取当前登录用户的逻辑
logEntity.setUsername("admin");
logEntity.setUserId(1L);
} catch (Exception e) {
// 忽略获取用户失败,防止日志影响业务
}
// 获取IP地址
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
logEntity.setIpAddress(request.getRemoteAddr());
logEntity.setCreateTime(new Date());
// 异步保存日志
auditLogService.saveLogAsync(logEntity);
}
}
```
五、实体类与异步持久化服务

为了保证审计日志的记录不影响主业务的响应速度,日志的入库操作必须异步执行。我们需要配置线程池并编写Service层代码。
在启动类或配置类上开启异步支持:
```java
@EnableAsync
@SpringBootApplication
public class ArchiveApplication {
public static void main(String[] args) {
SpringApplication.run(ArchiveApplication.class, args);
}
// 配置异步线程池,防止使用默认线程池导致OOM
@Bean(name = "auditTaskExecutor")
public Executor auditTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("Audit-Log-");
executor.initialize();
return executor;
}
}
```
接着,编写SysAuditLogService,使用@Async注解指定线程池:
```java
package com.example.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.SysAuditLog;
import com.example.mapper.SysAuditLogMapper;
import com.example.service.SysAuditLogService;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class SysAuditLogServiceImpl extends ServiceImpl
implements SysAuditLogService {
@Override
@Async("auditTaskExecutor") // 指定上面配置的线程池
public void saveLogAsync(SysAuditLog logEntity) {
// 这里直接调用MyBatis Plus的save方法
this.save(logEntity);
}
}
```
六、业务层集成实操
完成上述配置后,在档案管理的Controller或Service方法上,只需添加@AuditLog注解即可自动生成审计日志。
以下是一个档案上传和删除的Controller示例:
```java
package com.example.controller;
import com.example.archive.annotation.AuditLog;
import org.springframework.web.bind.annotation.;
import org.springframework.web.multipart.MultipartFile;
@RestController
@RequestMapping("/api/archive")
public class ArchiveController {
@PostMapping("/upload")
// 关键点:添加注解,声明这是“档案管理”模块的“上传文件”操作
@AuditLog(module = "档案管理", action = "上传文件")
public String uploadFile(@RequestParam("file") MultipartFile file, @RequestParam("category") String category) {
// 模拟业务逻辑
if (file == null) {
throw new RuntimeException("文件不能为空");
}
// ... 执行保存逻辑 ...
return "文件上传成功,ID: " + System.currentTimeMillis();
}
@DeleteMapping("/delete/{id}")
// 关键点:添加注解,声明这是“档案管理”模块的“删除档案”操作
@AuditLog(module = "档案管理", action = "删除档案")
public String deleteArchive(@PathVariable("id") Long id) {
// 模拟业务逻辑
if (id == 1) {
throw new RuntimeException("系统核心档案禁止删除");
}
// ... 执行删除逻辑 ...
return "档案ID: " + id + " 已删除";
}
}
```
七、功能验证与排查
部署完成后,通过Postman或浏览器调用上述接口,观察数据库sys_audit_log表的数据变化。
- 成功场景验证:调用
/upload接口并传入正常文件。检查数据库,应生成一条status=1的记录,request_params包含文件名和分类,response_result包含返回ID。
- 失败场景验证:调用
/delete/1接口。检查数据库,应生成一条status=0的记录,error_msg字段应准确显示“系统核心档案禁止删除”。
- 性能验证:在Service的
saveLogAsync方法中打断点或让线程休眠2秒,再次调用接口,确认前端响应不需要等待这2秒,证明异步配置生效。