引言
在项目中,无论是在开发环境还是生产环境,如果系统出现故障,迎面而来的第一个问题往往是: "这个操作是谁在什么时候执行的?具体做了什么改动?" 如果系统对此一无所知,排查工作就如同大海捞针。
操作日志记录正是为了解决这一问题而生。它是一个系统性的、用于追踪用户行为、厘清操作责任以及复现历史流程的核心功能。它是系统的"黑匣子",也是开发者的"记事本"。 在SpringBoot项目中,若将日志记录代码散乱地嵌入到每个业务方法的try-catch块中,会导致代码重复严重、核心业务逻辑被污染、不利于维护。
本文将探讨如何利用SpringBoot面向切面编程(AOP) 来优雅地解决这一难题。
一、环境准备与项目搭建
1. 创建一个标准的SpringBoot项目
我使用的是SpringBoot3.5.5+JDK17
2. 引入核心依赖
- mysql-connector-j(数据库驱动)
- mybatis-plus-spring-boot3-starter(数据持久化框架)
- spring-boot-starter-aop(AOP核心)
- aspectjweaver(面向切面工具)
- spring-boot-starter-web(Web项目)
- fastjson2(阿里json解析器)
二、实现步骤
1. 创建日志实体类(OperateLog)
java
@Data
public class OperateLog {
private Long id; //主键
private Long userId; //操作人id
private Date createTime; //创建时间
private String description; //请求描述
private String method; //请求方法 如:post、get
private String ip; //操作人ip
private String param; //请求参数:json格式
private String result; //请求结果:json格式
private Integer success; //操作是否成功
private String errorMsg; //错误信息
}
2. 创建操作日志记录表(operate_log)
sql
create table operate_log(
id BIGINT PRIMARY key AUTO_INCREMENT COMMENT '主键',
user_id BIGINT DEFAULT 100 COMMENT '操作人id',
create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
description VARCHAR(200) COMMENT '请求描述',
method VARCHAR(20) COMMENT '请求方法 如:post、get',
ip VARCHAR(32) COMMENT '操作人ip',
param VARCHAR(2000) COMMENT '请求参数:json格式',
result VARCHAR(2000) COMMENT '请求结果:json格式',
success TINYINT DEFAULT 0 COMMENT '操作是否成功:1是、0否',
error_msg VARCHAR(2000) COMMENT '错误消息'
) COMMENT '操作日志记录表';
3. 创建添加日志的相关代码
java
//mapper接口
public interface OperateLogMapper extends BaseMapper<OperateLog> {}
//service接口
public interface OperateLogService extends IService<OperateLog> {}
//service实现类
@Service
public class OperateLogServiceImpl extends ServiceImpl<OperateLogMapper, OperateLog> implements OperateLogService {}
4. 创建自定义注解(@Log)
在注解类中,指定其可以对方法进行修饰,并设置运行时保留策略
java
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
/**
* 请求描述
*/
String title() default "";
}
5. 创建切面类(LogAspect)- 核心
在切面类中,创建前置通知和后置通知,并指定被@Log
注解修饰的方法为切点,实现对目标方法的"增强"。
java
@Aspect
@Component
public class LogAspect {
private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
@Autowired
private OperateLogService logService;
private final ExecutorService executor = Executors.newSingleThreadExecutor();
/**
* 请求前执行
* @param aspLog
*/
@Before(value = "@annotation(aspLog)")
public void doBefore(Log aspLog){
log.info("请求日志记录start");
}
/**
* 请求后执行
* @param aspLog
* @param result
*/
@AfterReturning(pointcut = "@annotation(aspLog)",returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Log aspLog, Object result){
handleLog(joinPoint,aspLog,null,result);
}
/**
* 请求异常执行
* @param aspLog
* @param e
*/
@AfterThrowing(value = "@annotation(aspLog)", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint,Log aspLog,Exception e){
handleLog(joinPoint,aspLog,e,null);
}
/**
* 日志处理器-记录日志
* @param aspLog
* @param e
* @param result
*/
private void handleLog(JoinPoint joinPoint,Log aspLog,Exception e,Object result){
ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
OperateLog operateLog = new OperateLog();
operateLog.setIp(attributes.getRequest().getRemoteAddr());
operateLog.setMethod(attributes.getRequest().getMethod());
String param = JSON.toJSONString(joinPoint.getArgs());
operateLog.setParam(param);
if(e != null){
operateLog.setErrorMsg(e.getLocalizedMessage());
}else{
operateLog.setSuccess(1);
operateLog.setResult(JSON.toJSONString(result));
}
operateLog.setDescription(aspLog.title());
executor.submit(() -> logService.save(operateLog));
}
}
6. 编写控制层代码进行测试
java
@RestController
public class TestController {
@Log(title = "测试操作")
@GetMapping("/test")
public String test(){
return "没毛病";
}
@Log(title = "测试参数操作")
@PostMapping("/testParam")
public String testParam(@RequestBody String data){
return data;
}
@Log(title = "测试异常操作")
@GetMapping("/testExecption")
public String testExecption(){
return (1/0)+"";
}
}
7. 其他注意事项
如果你使用的Springboot也是3.0+,在引入mybatis-plus依赖的时候,一定要导入mybatis-plus-spring-boot3-starter
,而不是mybatis-plus-boot-starter
。如果导入的依赖不正确,会导致Spring无法注入mybatis相关的内容并报错。
三、功能测试与验证(Postman)
测试三种情况,1、测试无参get请求,2、测试传参post请求,3、测试异常请求,对于成功的请求要在日志记录中标记为成功,对于异常的请求要记录异常信息。
1. 测试无参get请求

2. 测试post带参数请求

3. 测试异常请求

四、 总结
到这里,就完成了操作日志的记录功能,需要注意的是,在日常开发中,一定要将记录日志的代码交给另一个线程执行,避免持久化操作对主线程产生性能上的影响。面向切面的本质上是对切点方法做了一层代理,在不影响业务代码的前提下对其进行功能扩展,降低了业务代码和非业务代码的耦合度,这种设计适用于很多其他的业务场景,比如异常处理、接口性能监控、参数校验与预处理等。