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