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更好的实现方式,也请给出建议,例如后置过滤器逻辑。
响应装饰器未触发的核心原因
过滤器顺序优先级不足
当前设置的NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1可能不足以确保装饰器在响应写出前执行。如果存在其他默认过滤器(如TokenRelay)或路由内过滤器的order值更接近NettyWriteResponseFilter,会导致响应在你的装饰器生效前就被输出到客户端。201响应无响应体
HTTP 201 Created响应通常可能不携带响应体,此时writeWith方法不会被调用——因为没有内容需要写入。你可以通过curl或API调试工具确认下游服务返回的201响应是否确实包含响应体。响应对象被后续过滤器覆盖
在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

