一、问题
前端小程序通过springcloud gateway接入并访问后端的诸多微服务,几十个微服务相关功能均正常,只有小程序到后端推送服务的websocket连接建立不起来,使用whireshark抓包,发现在小程序通过 GET ws://192.168.6.100:8888/websocket发起的连接请求,网关返回404错误,并且发现在推送服务并未收到网关转发的任何数据,所以判断是网关的路由出了问题。通过搜索找到了springcloud gateway的关键类DispacherHandler,并首先从其handler方法入手,使用DEBUG手段,定位到问题为:
。以下针对关键代码进行梳理。
二、源码分析
1)DispacherHandler的handle方法
@Override
public Mono<Void> handle(ServerWebExchange exchange) {
if (this.handlerMappings == null) {
return createNotFoundError();
}
return Flux.fromIterable(this.handlerMappings)
.concatMap(mapping -> mapping.getHandler(exchange))
.next()
.switchIfEmpty(createNotFoundError())
.flatMap(handler -> invokeHandler(exchange, handler))
.flatMap(result -> handleResult(exchange, result));
}
1)调用Flux.fromIterable把handlerMapping转换为一个Flux流,
2)针对流中每个元素(RouteFunctionMapping、RequestMappingHandlerMapping和RoutePredictHandlerMapping)调用getHandler方法(该方法在AbstractHandlerMapping中实现),具体代码如下:
public Mono<Object> getHandler(ServerWebExchange exchange) {
return getHandlerInternal(exchange).map(handler -> {
if (logger.isDebugEnabled()) {
logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);
}
ServerHttpRequest request = exchange.getRequest();
if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
CorsConfiguration config = (this.corsConfigurationSource != null ? this.corsConfigurationSource.getCorsConfiguration(exchange) : null);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);
config = (config != null ? config.combine(handlerConfig) : handlerConfig);
if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {
return REQUEST_HANDLED_HANDLER;
}
}
return handler;
});
}
3)调用RoutePredictHandlerMapping本类的getHandlerInternel方法
4)getHandlerInternel方法调用本类的lookupRoute方法,lookupRoute方法关键代码如下:
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {
return this.routeLocator.getRoutes()
// individually filter routes so that filterWhen error delaying is not a
// problem
.concatMap(route -> Mono.just(route).filterWhen(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
filterWhen的条件为:
(r -> {
// add the current route we are testing
exchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());
return r.getPredicate().apply(exchange);
})
其中r是路由,r.getPredicate()为AsyncPredicate,其apply方法为:
public Publisher<Boolean> apply(T t) {
return Mono.just(delegate.test(t));
}
其中的delegate为PathRoutePredictFactory,t为DefaultServerWebExchange,PathRoutePredictFactory的test方法为:
return new GatewayPredicate() {
@Override
public boolean test(ServerWebExchange exchange) {
PathContainer path = parsePath(
exchange.getRequest().getURI().getRawPath());
PathPattern match = null;
for (int i = 0; i < pathPatterns.size(); i++) {
PathPattern pathPattern = pathPatterns.get(i);
if (pathPattern.matches(path)) {
match = pathPattern;
break;
}
}
其中path从Request提取的URI的rawPath,pathPatterns是application.yml配置的path,遍历查找,命中则跳出并返回true,否则返回false。debug代码发现请求地址的path匹配成功。至此未发现问题。
5.转机出现
在application.yml中意外的发现针对WebSocket的请求,不知是哪位大神增加了一个ReadBodyString断言,从网上看到使用ReadBodyString断言时,如果请求的HTTP BODY为空,则返回404错误,这给了我提示。正好手上有websocket建立建立失败的网络抓包报文,报文中的BODY确实为空,具体如下:
按图索骥,找到了ReadBodyPredicateFactory类的applyAsync方法,在其中针对BODY为空的处理逻辑如下:
public AsyncPredicate<ServerWebExchange> applyAsync(Config config) {
……
return ServerWebExchangeUtils.cacheRequestBodyAndRequest(exchange, (serverHttpRequest) -> ServerRequest .create(exchange.mutate().request(serverHttpRequest) .build(), messageReaders) .bodyToMono(inClass) #问题在这里!!!! .doOnNext(objectValue -> exchange.getAttributes().put( CACHE_REQUEST_BODY_OBJECT_KEY, objectValue)) .map(objectValue -> config.getPredicate() .test(objectValue)));
继续跟踪其中的bodyToMono方法,代码如下:
public <T> Mono<T> bodyToMono(Class<? extends T> elementClass) { Mono<T> mono = body(BodyExtractors.toMono(elementClass)); #问题在这里!!! return mono.onErrorMap(UnsupportedMediaTypeException.class, ERROR_MAPPER) .onErrorMap(DecodingException.class, DECODING_MAPPER); }
其中的body方法用来解析BODY,但因为BODY为空,所以抛出了unsupportedMediaTypeException,在onErrorMap中处理该异常,所以最终路由未找到,客户端收到404错误。