核心思想AOP 异步 事件驱动我们的目标是当一个被监控的API被调用时主业务逻辑能够快速响应而日志记录等辅助任务则在后台悄然完成。AOP面向切面编程:用于在不侵入原有代码的情况下拦截特定的方法调用如Controller中的方法。通过定义切入点Pointcut和通知Advice我们可以精确地知道何时何地需要触发日志记录行为。事件驱动Event-Driven:当AOP拦截到目标方法调用后不直接执行耗时的日志保存操作而是发布一个“API调用完成”的事件。这种方式将触发动作方法调用与处理逻辑日志保存解耦。异步处理Asynchronous Processing:专门的监听器Listener订阅上述事件。这些监听器被标记为Async意味着它们将在独立的线程池中执行彻底释放主线程让其专注于业务逻辑的处理。这种组合模式的优势在于它既利用了AOP的非侵入性和灵活性来定位需要监控的点又通过事件驱动实现了松耦合最终依靠异步处理保证了主线程的高效运行。3.代码实现3.1 环境依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency日志实体类这个可根据直接要求修改Data TableName(sys_log) public class SysLog implements Serializable { // 实现 Serializable 是一个好的实践 private static final long serialVersionUID 1L; /** ID */ // 指定主键并使用数据库自增策略 private String id; /** 日志类型 */ TableField(log_type) private String logType; /** 创建用户编码 */ TableField(create_user_code) private String createUserCode; /** 创建用户名称 */ TableField(create_user_name) private String createUserName; /** 创建时间 */ TableField(create_date) private LocalDateTime createDate; /** 请求URI */ TableField(request_uri) private String requestUri; /** 请求方式 */ TableField(request_method) private String requestMethod; /** 请求参数 */ TableField(request_params) private String requestParams; /** 响应参数 */ TableField(response_params) private String responseParams; /** 请求IP */ TableField(request_ip) private String requestIp; /** 请求服务器地址 */ TableField(server_address) private String serverAddress; /** 是否异常 (0: 正常, 1: 异常) */ TableField(is_exception) private String isException; /** 异常信息 */ TableField(exception_info) private String exceptionInfo; /** 开始时间 */ TableField(start_time) private LocalDateTime startTime; /** 结束时间 */ TableField(end_time) private LocalDateTime endTime; /** 执行时间 (毫秒) */ TableField(execute_time) private Integer executeTime; /** 用户代理 */ TableField(user_agent) private String userAgent; /** 操作系统 */ TableField(device_name) private String deviceName; /** 浏览器名称 */ TableField(browser_name) private String browserName; }3.2 在主程序开启异步在 主应用类或配置类上启用了异步支持EnableAsyncEnableAsync // -- 启用异步方法执行 SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(BomCompareApplication.class, args); } }3.3 编写切面切面负责拦截目标方法调用并在方法执行前后收集所需信息最后发布事件如果想监听自己定义的注解例如Pointcut(value annotation(com.xncoding.aop.aspect.UserAccess)) 具体可以参考之前的文章redis AOP 自定义注解实现接口限流 - 古渡蓝按 - 博客园Slf4j Aspect Component public class LogAspect { Autowired private AsyncLogService asyncLogService; // 注入异步服务 // 这个目前是对从 com.xncoding.aop.controller.* 下的都进行切入如果想对上面的自定义注解进行切入只需改成相对应的路径 // 例如Pointcut(value annotation(com.xncoding.aop.aspect.UserAccess)) Pointcut(execution(public * com.example.bomcompare.controller.*.*(..))) public void webLog(){} //环绕通知,环绕增强相当于MethodInterceptor Around(webLog()) public Object arround(ProceedingJoinPoint pjp) throws Throwable{ System.out.println(方法环绕start.....); // --- 1. 初始化日志信息 --- String requestId UUID.randomUUID().toString(); // 生成唯一请求ID LocalDateTime startTime LocalDateTime.now(); long startTimeMillis System.currentTimeMillis(); String url N/A; String method N/A; String ip N/A; String userAgent N/A; // 新增获取 User-Agent String className pjp.getSignature().getDeclaringTypeName(); String methodName pjp.getSignature().getName(); String params Arrays.toString(pjp.getArgs()); // 注意可能包含敏感信息 try { ServletRequestAttributes attributes (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes null) { log.warn(Not in a web request context, skipping request log.); } HttpServletRequest request attributes.getRequest(); url request.getRequestURL().toString(); method request.getMethod(); ip getClientIpAddress(request); userAgent request.getHeader(User-Agent); // 获取 User-Agent // --- 3. 记录开始日志 (可选用于调试) --- log.info(Captured Request [ID: {}]: URL{}, Method{}, IP{}, Class.Method{}.{}, Args{}, requestId, url, method, ip, className, methodName, params); } catch (Exception e) { log.warn(Could not extract HTTP request details in aspect., e); } // --- 4. 执行目标方法并捕获结果/异常 --- Object result null; String errorMessage null; Exception caughtException null; // 用于存储捕获到的异常对象 try { result pjp.proceed(); // 执行被拦截的方法 } catch (Exception ex) { // 捕获所有检查和非检查异常 caughtException ex; errorMessage ex.getClass().getSimpleName() : ex.getMessage(); // 记录简化的错误信息 // 重要必须重新抛出异常否则原调用方无法感知到异常的发生 throw ex; } finally { // --- 5. 计算执行时间和准备最终日志 --- long endTimeMillis System.currentTimeMillis(); long executionTime endTimeMillis - startTimeMillis; LocalDateTime endTime LocalDateTime.now(); String resultStr N/A; if (result ! null) { // 注意直接 toString() 大对象可能导致性能问题或日志过大 // 可以考虑限制长度或根据类型特殊处理 resultStr result.toString(); } // 将所有收集到的信息传递给异步服务 asyncLogService.saveRequestLog( requestId, url, method, ip, className, methodName, params, resultStr, errorMessage, executionTime, startTime, endTime, userAgent // 传递 User-Agent ); // 返回目标方法的原始结果 return result; } } // --- 辅助方法获取客户端真实IP --- private String getClientIpAddress(HttpServletRequest request) { String xfHeader request.getHeader(X-Forwarded-For); if (xfHeader ! null !xfHeader.isEmpty() !unknown.equalsIgnoreCa