SpringCloud Gateway的一次踩坑

381 阅读4分钟

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信息,以致抛异常。那么解决方法也很简单,把服务名称中的下划线改为中划线或者去掉都可以。