1、业务需求
接口文档日志模块:
GET /sys/log/list:分页查询系统操作日志,支持用户名、时间范围搜索
DELETE /sys/log/del:批量删除日志记录
日志字段:用户名、操作描述、调用全类名方法、请求参数、执行耗时 ms、ip、创建时间。
如果每个新增、修改、删除接口手写日志,代码大量重复。AOP 切面拦截,加注解即可自动记录。
日志数据库表:
CREATE TABLE sys_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
username VARCHAR(100) COMMENT '操作人用户名',
operation VARCHAR(200) COMMENT '操作描述',
method VARCHAR(300) COMMENT '执行方法全路径',
params TEXT COMMENT '请求参数JSON',
time BIGINT COMMENT '执行耗时(ms)',
ip VARCHAR(100) COMMENT '访问IP',
create_time DATETIME COMMENT '操作时间'
)ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2、自定义注解 @SysLog
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SysLog {
/**操作描述*/
String value();
}
3、AOP 切面类,环绕通知 @Around
使用异步线程池保存日志,不阻塞主业务请求。
@Aspect
@Component
@Slf4j
public class SysLogAspect {
@Autowired
private SysLogMapper sysLogMapper;
//线程池,异步保存日志
private final Executor executor = Executors.newFixedThreadPool(3);
//切点:所有打上@SysLog注解的方法
@Pointcut("@annotation(com.villege.aop.SysLog)")
public void pointCut(){}
@Around("pointCut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
//执行目标方法
Object result = joinPoint.proceed();
long costTime = System.currentTimeMillis() - startTime;
//异步记录日志,不阻塞接口返回
executor.execute(()->{
try {
saveSysLog(joinPoint,costTime);
}catch (Exception e){
log.error("保存操作日志异常",e);
}
});
return result;
}
private void saveSysLog(ProceedingJoinPoint joinPoint, long costTime) {
SysLogEntity logEntity = new SysLogEntity();
//拿到注解上的操作描述
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
SysLog annotation = signature.getMethod().getAnnotation(SysLog.class);
logEntity.setOperation(annotation.value());
//方法全限定名
logEntity.setMethod(signature.getMethod().toString());
//请求参数
Object[] args = joinPoint.getArgs();
logEntity.setParams(JSON.toJSONString(args));
logEntity.setTime(costTime);
//获取request对象,拿IP
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attr.getRequest();
logEntity.setIp(getIpAddr(request));
//从request拿登录userId,查询用户名
Long userId = (Long)request.getAttribute("userId");
User user = userMapper.selectById(userId);
logEntity.setUsername(user.getUsername());
logEntity.setCreateTime(new Date());
sysLogMapper.insert(logEntity);
}
//获取真实客户端IP工具方法
public static String getIpAddr(HttpServletRequest request) {
String ip = request.getHeader("x-forwarded-for");
if(ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
return ip;
}
}
4、Controller 中使用,零侵入业务代码
@RestController
@RequestMapping("/sys/community")
public class CommunityController {
@Autowired
private CommunityService communityService;
//添加注解,value写操作描述,切面自动记录日志
@SysLog(value = "新增小区信息")
@PostMapping("/add")
public Result add(@RequestBody Community community){
communityService.save(community);
return Result.success();
}
}
5、踩坑总结
RequestContextHolder 获取 request,异步线程拿不到 Request 对象:必须在主线程先获取 request 相关数据再丢到异步线程;
不要把大文件 MultipartFile 直接 JSON 序列化,会报错;参数序列化需要做过滤;
线程池建议自定义 ThreadPoolExecutor,不要直接 Executors,生产防止 OOM;
AOP 只能拦截 Spring 容器中的 Bean,private 私有方法不会被 AOP 增强;
如果接口抛出异常,@Around 依然可以捕获,记录异常操作日志。
拓展:可以捕获异常,记录异常堆栈信息存入日志,方便排查线上 BUG。