Spring Boot WebFlux AOP切面如何适配Mono类型请求参数
现有实现的核心问题
- 切点表达式用
&& args(request)直接绑定方法入参,当入参改为Mono<TransactionInsightsData>时,切面拿到的request是未订阅的Mono冷流对象,不是实际反序列化后的请求实体,直接打日志、存审计记录拿到的都是Mono对象本身,拿不到真实请求参数。 - 原逻辑末尾的
.map(o -> result)是明显的逻辑错误:该操作会把流中实际的响应对象替换为原始Mono实例,会导致接口返回序列化异常、响应内容错误。 - 在
doOnSuccess/doOnError回调中手动调用.subscribe()触发审计日志保存,不符合响应式编程规范:手动启动的订阅脱离WebFlux管理的请求生命周期,会丢失Reactor上下文(比如链路追踪ID、请求自定义上下文),还可能出现请求已返回但日志写入未完成、上下文销毁导致空指针等问题。 - 逻辑没有做参数类型兼容,只能处理普通实体入参,无法识别Mono包装的请求体。
改造后的实现代码
@Slf4j @Aspect @Order(3) @Component @RequiredArgsConstructor public class AuditLogEventAspect { private final AuditEventManager auditEventManager; @Around("@annotation(auditLogEvent)") public Object logAround(ProceedingJoinPoint joinPoint, AuditLogEvent auditLogEvent) throws Throwable { Object[] args = joinPoint.getArgs(); Mono<Object> requestMono = Mono.empty(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Parameter[] parameters = signature.getMethod().getParameters(); // 识别@RequestBody标注的参数,统一转换为Mono格式的真实请求流 for (int i = 0; i < parameters.length; i++) { Parameter param = parameters[i]; Object arg = args[i]; if (arg == null) continue; if (param.isAnnotationPresent(RequestBody.class)) { if (arg instanceof Mono<?> monoArg) { // Mono类型入参加cache,避免多次订阅导致请求体重复解析 requestMono = monoArg.map(req -> (Object) req).cache(); // 替换原参数为cache后的Mono,保证原方法能正常读取请求体 args[i] = requestMono; } else { // 普通实体入参直接包装为Mono requestMono = Mono.just(arg); } break; } } // 执行原方法获取响应流 Mono<?> responseMono = (Mono<?>) joinPoint.proceed(args); Mono<Object> finalRequestMono = requestMono; return Mono.deferContextual(ctx -> { Map<String, String> contextMap = ctx.get(CONTEXT_MAP); return responseMono // 处理正常响应场景 .flatMap(response -> finalRequestMono .flatMap(request -> logSuccessExit(auditLogEvent, contextMap, request, response)) .doOnNext(auditEvent -> log.info("auditEvent {}", auditEvent)) // 日志写入不影响原响应返回 .thenReturn(response) ) // 处理异常场景 .onErrorResume(e -> finalRequestMono .flatMap(request -> logErrorExit(auditLogEvent, contextMap, request, (Exception) e)) .doOnNext(auditEvent -> log.info("auditEvent {}", auditEvent)) // 日志写入完成后继续抛出原异常,不破坏原异常处理逻辑 .then(Mono.error(e)) ); }); } private Mono<AuditEvent> logErrorExit(AuditLogEvent auditLogEvent, Map<String, String> contextMap, Object request, Exception error) { log.info("TRA request: {}", request); log.info("TRA error response: {}", error.getMessage()); AuditEvent initialAuditEvent = auditEventManager.createAuditLogEvent(auditLogEvent, contextMap, request); return auditEventManager.saveExceptionEvent(initialAuditEvent, error); } private Mono<AuditEvent> logSuccessExit(AuditLogEvent auditLogEvent, Map<String, String> contextMap, Object request, Object response) { log.info("TRA request: {}", request); log.info("TRA response: {}", response); AuditEvent initialAuditEvent = auditEventManager.createAuditLogEvent(auditLogEvent, contextMap, request); return auditEventManager.saveSuccessEvent(initialAuditEvent, response); } }
关键改造说明
- 去掉了切点表达式中直接绑定入参的
&& args(request)逻辑,改为通过方法签名识别@RequestBody标注的参数,同时兼容普通实体、Mono包装实体两种入参形式。 - 对Mono类型的入参添加
cache()处理,避免Mono被多次订阅导致请求体重复反序列化、流被消费后原方法无法读取参数的问题。 - 删除了原代码中错误的
.map(o -> result)逻辑,通过thenReturn(response)保证原接口响应值正常返回,不会破坏接口返回结构。 - 移除了手动
.subscribe()触发日志写入的逻辑,将审计日志保存逻辑通过flatMap串入主请求响应流,由WebFlux框架统一负责订阅调度,完全继承原流的Reactor上下文,不会出现链路信息丢失、生命周期不一致的问题。 - 异常场景下通过
then(Mono.error(e))保证原异常正常抛出,切面不会吞掉异常、改变原接口的错误处理逻辑。
注:如果后续需要支持Flux类型的流式请求体,只需要把对应Mono的处理逻辑替换为Flux即可,整体思路一致。
内容的提问来源于stack exchange,提问作者A A
相关产品推荐
相关产品推荐

