You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Cloud Gateway中ServerHttpResponseDecorator的writeWith未触发问题排查

问题

我有一个Spring Cloud Gateway应用,想要实现一个过滤器,在响应返回客户端之前读取响应体。以下是自定义过滤器的代码:

@Component
public class CreateResourceGatewayFilterFactory extends AbstractGatewayFilterFactory<CreateResourceGatewayFilterFactory.Config> implements Ordered {

    private static final Logger log = LoggerFactory.getLogger(CreateResourceGatewayFilterFactory.class);
    
    public static class Config {

        private ResourceType resourceType;
        private String requiredRoles;

        public ResourceType getResourceType() {
            return resourceType;
        }
        public void setResourceType(ResourceType resourceType) {
            this.resourceType = resourceType;
        }

        public String getRequiredRoles() {
            return requiredRoles;
        }
        public void setRequiredRoles(String requiredRoles) {
            this.requiredRoles = requiredRoles;
        }
    }

    public CreateResourceGatewayFilterFactory() {
        super(Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        List<String> order = new ArrayList<>(2);
        order.add("resourceType");
        order.add("requiredRoles");
        return order;
    }

    @Override
    public GatewayFilter apply(Config config) {

        return (exchange, chain) -> {
            log.debug("Received request at CreateResource filter.");
            return chain.filter(exchange.mutate().request(getDecoratedRequest(exchange.getRequest())).response(getDecoratedResponse(exchange.getResponse(), config.getResourceType())).build());
        };
    }

    private ServerHttpResponseDecorator getDecoratedResponse(ServerHttpResponse response, ResourceType resourceType) {
        return new ServerHttpResponseDecorator(response) {
            @Override
            public Mono<Void> writeWith(final Publisher<? extends DataBuffer> body) {
                log.debug("Inside response decorator");
                if (body instanceof Flux) {
                    log.debug("Received response from microservice.");
                    Flux<? extends DataBuffer> fluxBody = (Flux<? extends DataBuffer>) body;
                    return super.writeWith(fluxBody.buffer()
                        .map(dataBuffers -> {
                            DefaultDataBuffer joinedBuffers = new DefaultDataBufferFactory().join(dataBuffers);
                            byte[] content = new byte[joinedBuffers.readableByteCount()];
                            joinedBuffers.read(content);
                            String responseBody = new String(content, StandardCharsets.UTF_8);
                            log.debug("Buffered response parsed to String {}", responseBody);
                            return responseBody;
                        })
                        .flatMap(responseBody -> doSomething(resourceType, responseBody))
                        .map(responseBody -> response.bufferFactory().wrap(responseBody.getBytes())))
                    .onErrorResume(err -> {
                        log.error("Error extracting resoruce from message {}",err.getMessage());
                        return Mono.empty();
                    });
                }
                return super.writeWith(body);
            }
        };
    }

    private Flux<String> doSomething(ResourceType resourceType, String payload) {
        // DO SOMETHING WITH PAYLOAD
        return Flux.just(payload);
    }

    private ServerHttpRequest getDecoratedRequest(ServerHttpRequest request) {
        return new ServerHttpRequestDecorator(request) {
            @Override
            public Flux<DataBuffer> getBody() {
                log.debug("requestId: {}, method: {} , url: {}", request.getId(), request.getMethodValue(), request.getURI());
                return request.getBody();
            }
        };
    }

    @Override
    public int getOrder() {
        return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
    }
}

application.yml中的DSL配置如下:

spring:
  cloud:
    gateway:
      default-filters:
        - TokenRelay
      discovery:
        locator:
          enabled: true
          lower-case-service-id: true
          predicates:
            - name: Path
              args:
                pattern: "'/services/'+serviceId.toLowerCase()+'/**'"
          filters:
            - name: RewritePath
              args:
                regexp: "'/services/' + serviceId.toLowerCase() + '/(?<remaining>.*)'"
                replacement: "'/${remaining}'"
      routes:
        - id: sample_route
          uri: http://localhost:8097
          predicates:
            - Method=POST
            - Path=/api/resources/**
          filters:
            - CreateResource=DEVICE,SUPER_ADMIN

请求能被过滤器和请求装饰器捕获(可看到对应日志),但响应装饰器无日志输出,响应状态码为201。我需要了解响应装饰器未触发的原因。

注:该过滤器的目的是读取响应体,此过程可在独立线程中运行,无需在返回客户端前修改响应体。若有比ServerHttpResponseDecorator更好的实现方式,也请给出建议,例如后置过滤器逻辑。


解答

响应装饰器未触发的核心原因

  1. 过滤器顺序优先级不足
    当前设置的NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1可能不足以确保装饰器在响应写出前执行。如果存在其他默认过滤器(如TokenRelay)或路由内过滤器的order值更接近NettyWriteResponseFilter,会导致响应在你的装饰器生效前就被输出到客户端。

  2. 201响应无响应体
    HTTP 201 Created响应通常可能不携带响应体,此时writeWith方法不会被调用——因为没有内容需要写入。你可以通过curl或API调试工具确认下游服务返回的201响应是否确实包含响应体。

  3. 响应对象被后续过滤器覆盖
    在apply方法中通过exchange.mutate().response(...)替换响应后,若后续过滤器链中再次修改了响应对象,你的装饰器会被覆盖,无法生效。

针对性修复方案

1. 调整过滤器顺序

将过滤器的order设置为比NettyWriteResponseFilter的order更小的数值,确保它在响应写出前优先执行:

@Override
public int getOrder() {
    // 比NettyWriteResponseFilter提前两个优先级,避免被其他过滤器抢占
    return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 2;
}

若需要最高优先级,可直接使用Ordered.HIGHEST_PRECEDENCE,但需注意与其他高优先级过滤器的兼容性。

2. 处理无响应体的场景

重写setComplete方法,因为当响应无体时,网关会调用setComplete而非writeWith:

@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
    // 保留原有逻辑
}

@Override
public Mono<Void> setComplete() {
    log.debug("Handling empty response body (201 Created)");
    doSomething(resourceType, "");
    return super.setComplete();
}

3. 确认响应体存在

通过调试工具验证下游服务返回的201响应是否包含响应体,若确实无体,可根据业务需求决定是否触发后续逻辑。

更优实现方案:异步后置读取响应体

如果仅需异步读取响应体、不修改响应,可以采用无需装饰器的后置逻辑,避免装饰器被覆盖的问题,同时不阻塞响应返回客户端:

@Override
public GatewayFilter apply(Config config) {
    return (exchange, chain) -> {
        log.debug("Received request at CreateResource filter.");
        // 先执行过滤器链,完成响应处理
        return chain.filter(exchange)
                // 异步读取响应体,不阻塞响应返回
                .then(Mono.defer(() -> {
                    ServerHttpResponse response = exchange.getResponse();
                    if (response.getStatusCode().is2xxSuccessful()) {
                        return response.getBody()
                                .aggregate()
                                .map(dataBuffer -> {
                                    byte[] content = new byte[dataBuffer.readableByteCount()];
                                    dataBuffer.read(content);
                                    return new String(content, StandardCharsets.UTF_8);
                                })
                                .flatMap(body -> doSomething(config.getResourceType(), body))
                                .then();
                    }
                    return Mono.empty();
                }));
    };
}

这种方式利用Reactor的then操作符在响应处理完成后异步读取响应体,完全符合你“独立线程运行”的需求,代码更简洁且不易出错。


内容的提问来源于stack exchange,提问作者user6952691

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 20:35:01