一、问题
前端小程序通过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 testingexchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());return r.getPredicate().apply(exchange);})
filterWhen的条件为:
(r -> {// add the current route we are testingexchange.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() {@Overridepublic 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错误。