Spring AOP 自定义注解实现系统操作日志

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。

相关推荐
金伟API10242 小时前
不会写复杂SQL怎么办?
数据库·sql
执笔为剑2 小时前
2.1.2-1、V8R3迁移到V8R6-Oracle兼容特性
数据库
geovindu3 小时前
sql:Search-Optimized SQL Queries using mysql 9.0
android·数据库·sql·mysql·数据库开发·数据库架构
for_ever_love__3 小时前
MySQL SELECT 与 WHERE 优化实战:可搜索条件、联合索引与执行计划
数据库·mysql·oracle·select·where·条件优化·查询优化技巧
2501_931803753 小时前
MySQL 锁机制详解
数据库·mysql
Nturmoils3 小时前
SQL 编辑器不只是写查询:我用 KStudio 排查一次订单异常
数据库
IvorySQL4 小时前
PostgreSQL 日报|逻辑解码 DDL 子事务缺陷修复(10 月 12 日)
数据库·postgresql
这个DBA有点耶4 小时前
研发规模/投入最大的国产数据库厂商是谁?拆解五个衡量维度
数据库·程序员·产品
葡萄城技术团队4 小时前
数据库里的数据明明是对的,为什么 BI 报表还是错?
数据库