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)都可以。

posted on 2022-12-31 20:46  小夏coding  阅读(548)  评论(0编辑  收藏  举报

导航