SpringCloud Gateway的一次踩坑
在一次使用SpringCloud Gateway做网关时,向网关发出URL请求,结果网关在路由时报错:
java.lang.IllegalStateException: Invalid host: lb://ORDER_SERVICE
根据报错堆栈信息,找到抛异常的代码在RouteToRequestUrlFilter文件的filter方法:
1 public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { 2 Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); 3 if (route == null) { 4 return chain.filter(exchange); 5 } 6 log.trace("RouteToRequestUrlFilter start"); 7 URI uri = exchange.getRequest().getURI(); 8 boolean encoded = containsEncodedParts(uri); 9 URI routeUri = route.getUri(); 10 11 if (hasAnotherScheme(routeUri)) { 12 // this is a special url, save scheme to special attribute 13 // replace routeUri with schemeSpecificPart 14 exchange.getAttributes().put(GATEWAY_SCHEME_PREFIX_ATTR, 15 routeUri.getScheme()); 16 routeUri = URI.create(routeUri.getSchemeSpecificPart()); 17 } 18 19 // 断点跟踪routeUri的值为“lb://ORDER_SERVICE”,并且host为null 20 if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) { 21 // Load balanced URIs should always have a host. If the host is null it is 22 // most 23 // likely because the host name was invalid (for example included an 24 // underscore) 25 throw new IllegalStateException("Invalid host: " + routeUri.toString()); 26 } 27 28 URI mergedUrl = UriComponentsBuilder.fromUri(uri) 29 // .uri(routeUri) 30 .scheme(routeUri.getScheme()).host(routeUri.getHost()) 31 .port(routeUri.getPort()).build(encoded).toUri(); 32 exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, mergedUrl); 33 return chain.filter(exchange); 34 }
断点跟踪routeUri的值为“lb://ORDER_SERVICE”,并且host为null,满足了if条件,所以抛出下面的异常。很明显问题的原因是host解析失败导致的。
在网关工程中并未去配置route,而是采用了eureka的注册中心动态配置,注册中心动态配置的定位器类是DiscoveryClientRouteDefinitionLocator,这个类会根据从eureka注册中心拉取到的服务动态生成RouteDefinition,buildRouteDefinition方法代码如下:
1 protected RouteDefinition buildRouteDefinition(Expression urlExpr, 2 ServiceInstance serviceInstance) { 3 String serviceId = serviceInstance.getServiceId(); 4 RouteDefinition routeDefinition = new RouteDefinition(); 5 routeDefinition.setId(this.routeIdPrefix + serviceId); 6 String uri = urlExpr.getValue(this.evalCtxt, serviceInstance, String.class); 7 routeDefinition.setUri(URI.create(uri)); 8 // add instance metadata 9 routeDefinition.setMetadata(new LinkedHashMap<>(serviceInstance.getMetadata())); 10 return routeDefinition; 11 }
其中routeDefinition.setUri(URI.create(uri)),这里会根据字符串“lb://ORDER_SERVICE”生成URI对象,生成代码:
1 public static URI create(String str) { 2 try { 3 return new URI(str); 4 } catch (URISyntaxException x) { 5 throw new IllegalArgumentException(x.getMessage(), x); 6 } 7 } 8 9 public URI(String str) throws URISyntaxException { 10 new Parser(str).parse(false); 11 } 12 13 void parse(boolean rsa) throws URISyntaxException { 14 requireServerAuthority = rsa; 15 int ssp; // Start of scheme-specific part 16 int n = input.length(); 17 int p = scan(0, n, "/?#", ":"); 18 if ((p >= 0) && at(p, n, ':')) { 19 if (p == 0) 20 failExpecting("scheme name", 0); 21 checkChar(0, L_ALPHA, H_ALPHA, "scheme name"); 22 checkChars(1, p, L_SCHEME, H_SCHEME, "scheme name"); 23 scheme = substring(0, p); 24 p++; // Skip ':' 25 ssp = p; 26 if (at(p, n, '/')) { 27 //parseHierarchical方法会调用parseHostname方法解析出host参数 28 p = parseHierarchical(p, n); 29 } else { 30 int q = scan(p, n, "", "#"); 31 if (q <= p) 32 failExpecting("scheme-specific part", p); 33 checkChars(p, q, L_URIC, H_URIC, "opaque part"); 34 p = q; 35 } 36 } else { 37 ssp = 0; 38 p = parseHierarchical(0, n); 39 } 40 schemeSpecificPart = substring(ssp, p); 41 if (at(p, n, '#')) { 42 checkChars(p + 1, n, L_URIC, H_URIC, "fragment"); 43 fragment = substring(p + 1, n); 44 p = n; 45 } 46 if (p < n) 47 fail("end of URI", p); 48 }
Parse中的parseHierarchical方法会调用parseHostname方法解析出host参数
1 private int parseHostname(int start, int n) throws URISyntaxException { 2 int p = start; 3 int q; 4 int l = -1; // Start of last parsed label 5 6 do { 7 // domainlabel = alphanum [ *( alphanum | "-" ) alphanum ] 8 //scan方法会从start处开始扫描出一个完整的名称,返回的q表示这个完整名称的最后一个字符的下标。 9 q = scan(p, n, L_ALPHANUM, H_ALPHANUM); 10 if (q <= p) 11 break; 12 l = p; 13 if (q > p) { 14 p = q; 15 q = scan(p, n, L_ALPHANUM | L_DASH, H_ALPHANUM | H_DASH); 16 if (q > p) { 17 if (charAt(q - 1) == '-') 18 fail("Illegal character in hostname", q - 1); 19 p = q; 20 } 21 } 22 q = scan(p, n, '.'); 23 if (q <= p) 24 break; 25 p = q; 26 } while (p < n); 27 28 if ((p < n) && !at(p, n, ':')) 29 fail("Illegal character in hostname", p); 30 31 if (l < 0) 32 failExpecting("hostname", start); 33 34 // for a fully qualified hostname check that the rightmost 35 // label starts with an alpha character. 36 if (l > start && !match(charAt(l), L_ALPHA, H_ALPHA)) { 37 fail("Illegal character in hostname", l); 38 } 39 40 host = substring(start, p); 41 return p; 42 }
scan方法会从start处开始扫描出一个完整的名称,返回的q表示这个完整名称的最后一个字符在“lb://ORDER_SERVICE”的下标:
1 private int scan(int start, int n, long lowMask, long highMask) throws URISyntaxException { 2 int p = start; 3 while (p < n) { 4 char c = charAt(p); 5 if (match(c, lowMask, highMask)) { 6 p++; 7 continue; 8 } 9 if ((lowMask & L_ESCAPED) != 0) { 10 int q = scanEscape(p, n, c); 11 if (q > p) { 12 p = q; 13 continue; 14 } 15 } 16 break; 17 } 18 return p; 19 }
scan方法中首先读取出位置p的字符c,然后判断c是否是允许的字符,循环读取,直到读取到不允许的字符,那么从start到p之间的字符就是要读取的完整的名称,那么判断字符是否是允许的字符的方法match的代码如下:
1 private static boolean match(char c, long lowMask, long highMask) { 2 if (c == 0) // 0 doesn't have a slot in the mask. So, it never matches. 3 return false; 4 if (c < 64) 5 return ((1L << c) & lowMask) != 0; 6 if (c < 128) 7 return ((1L << (c - 64)) & highMask) != 0; 8 return false; 9 }
这里的原理我没弄懂(可以参考文章https://blog.csdn.net/jiaobuchong/article/details/102757459),通过断点跟踪发现,一般的英文字符在这里都会返回true,但是下划线在这里就返回了false,于是读取的完整名称字符串就是下划线前面的字符串。在返回到parseHostname方法中有这么一行代码:
if ((p < n) && !at(p, n, ':')) fail("Illegal character in hostname", p);
这里的p表示刚才读取的下划线字符的下标,n表示字符串“lb://ORDER_SERVICE”的总长度,那么这行代码的意思就是如果p小于总长度并且p位置的字符不是符号“:”,则抛出异常。
到这里问题的原因真相大白了,就是服务名称“ORDER_SERVICE”中的下划线导致URI解析不出host信息,以致抛异常。那么解决方法也很简单,把服务名称order_service中的下划线改为中划线(order-service)或者去掉(orderService)都可以。