缘起
Zuul1.x已经不维护了,并且使用的BIO,当流量较大时性能下降的厉害,并且线程池中的线程用尽时如果某个请求返回了非200并且你没有配置处理过滤器的话,这个线程就假死了。公司的代码扫描工具也提示Zuul1.0里面有很多的jar已经过时了。Zuul2.x虽然修改BIO为NIO,但社区不活跃,没有和Spring兼容,性能也没有预期的好。Spring Cloud社区实现了自己的Gateway就是Spring Cloud Gateway,这里记录一下从Zuul1.x迁移到Spring Cloud Gateway 3.x的坑点。
踩坑
坑点1. java.lang.NoClassDefFoundError: org/springframework/core/metrics/ApplicationStartup
- 日志输出:
1Exception in thread "main" java.lang.NoClassDefFoundError: 2 at org.springframework.boot.SpringApplication.<init>(SpringApplication.java:232) 3 at org.springframework.boot.SpringApplication.<init>(SpringApplication.java:245) 4 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317) 5 at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306) 6 at com.example.gateway.GatewayApplication.main(GatewayApplication.java:12)
- 原因:netty本身版本不统一,netty和reactor版本不统一导致,因为我使用的是公司统一的父pom,并不是直接依赖的
spring-boot-parent。 我测试过,使用spring-boot-parent可以正常启动,无此错误 - 解决方案:引入bom(请替换成你自己的版本号):
1<dependency> 2 <groupId>org.springframework</groupId> 3 <artifactId>spring-framework-bom</artifactId> 4 <version>${spring-framework.version}</version> 5 <type>pom</type> 6 <scope>import</scope> 7</dependency> 8<dependency> 9 <groupId>io.netty</groupId> 10 <artifactId>netty-bom</artifactId> 11 <version>${netty.version}</version> 12 <type>pom</type> 13 <scope>import</scope> 14</dependency>
坑点2 java.lang.ClassNotFoundException: javax.servlet.Filter
- 日志输出:
1Caused by: java.lang.NoClassDefFoundError: 2 at java.lang.ClassLoader.defineClass1(Native Method) 3 at java.lang.ClassLoader.defineClass(ClassLoader.java:763) 4 at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) 5 at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) 6 at java.net.URLClassLoader.access$100(URLClassLoader.java:73) 7 at java.net.URLClassLoader$1.run(URLClassLoader.java:368) 8 at java.net.URLClassLoader$1.run(URLClassLoader.java:362) 9 at java.security.AccessController.doPrivileged(Native Method) 10 at java.net.URLClassLoader.findClass(URLClassLoader.java:361) 11 at java.lang.ClassLoader.loadClass(ClassLoader.java:424) 12 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) 13 at java.lang.ClassLoader.loadClass(ClassLoader.java:357) 14 at java.lang.ClassLoader.defineClass1(Native Method) 15 at java.lang.ClassLoader.defineClass(ClassLoader.java:763) 16 at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) 17 at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) 18 at java.net.URLClassLoader.access$100(URLClassLoader.java:73) 19 at java.net.URLClassLoader$1.run(URLClassLoader.java:368) 20 at java.net.URLClassLoader$1.run(URLClassLoader.java:362) 21 at java.security.AccessController.doPrivileged(Native Method) 22 at java.net.URLClassLoader.findClass(URLClassLoader.java:361) 23 at java.lang.ClassLoader.loadClass(ClassLoader.java:424) 24 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) 25 at java.lang.ClassLoader.loadClass(ClassLoader.java:357) 26 at java.lang.ClassLoader.defineClass1(Native Method) 27 at java.lang.ClassLoader.defineClass(ClassLoader.java:763) 28 at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142) 29 at java.net.URLClassLoader.defineClass(URLClassLoader.java:467) 30 at java.net.URLClassLoader.access$100(URLClassLoader.java:73) 31 at java.net.URLClassLoader$1.run(URLClassLoader.java:368) 32 at java.net.URLClassLoader$1.run(URLClassLoader.java:362) 33 at java.security.AccessController.doPrivileged(Native Method) 34 at java.net.URLClassLoader.findClass(URLClassLoader.java:361) 35 at java.lang.ClassLoader.loadClass(ClassLoader.java:424) 36 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) 37 at java.lang.ClassLoader.loadClass(ClassLoader.java:357) 38 at java.lang.Class.getDeclaredMethods0(Native Method) 39 at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) 40 at java.lang.Class.getDeclaredMethods(Class.java:1975) 41 at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:467) 42 ... 21 common frames omitted 43Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter 44 at java.net.URLClassLoader.findClass(URLClassLoader.java:381) 45 at java.lang.ClassLoader.loadClass(ClassLoader.java:424) 46 at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) 47 at java.lang.ClassLoader.loadClass(ClassLoader.java:357) 48 ... 61 common frames omitted
- 原因:这个应该是
Spring Cloud Gateway使用的是Spring Webflux而非Spring Web,导致javax包没有引入 - 解决方案:引入以下jar包
1<dependency> 2 <groupId>javax.servlet</groupId> 3 <artifactId>javax.servlet-api</artifactId> 4 <version>${your-version}</version> 5</dependency>
坑点3 server.servlet.contextPath不生效问题
- 原因:
Spring Cloud Gateway使用的是Spring Webflux,不是Spring Web,所以Spring Web的配置它无法解析 - 解决方案
- 方案1. 添加配置:
1spring: 2 webflux: 3 base-dir: /xxx- 方案2. 配置routor转发
1spring: 2 cloud: 3 gateway: 4 routes: 5 - id: self 6 uri: http://localhost:8080 7 predicates: 8 - Path=/xxx/** 9 filters: 10 # StripPrefix=1:去除原始请求路径中的前1级路径,去除2级的话StripPrefix=2 11 - StripPrefix=1- 方案3. 配置默认filter:
1 spring: 2 cloud: 3 gateway: 4 default-filters: 5 - StripPrefix=1
坑点4. spring.cloud.gateway.routes[0].uri带path无效
- 问题描述,配置如下(当前服务配置了
spring.webflux.base-dir=/xxx):
1spring: 2 cloud: 3 gateway: 4 routes: 5 - id: routeUser 6 uri: http://192.168.1.1:8081/dev/yyy 7 predicates: 8 - Path=/xxx/user/** 9 filters: 10 # StripPrefix:去除原始请求路径中的前1级路径 11 - StripPrefix=1
当使用post请求localhost:8080/xxx/user/getAllUser时,发现转发出去的是http://192.168.1.1:8081/getAllUser,而不是我期望的http://192.168.1.1:8081/dev/yyy/getAllUser
这种配置看着比较反常,其实在k8s容器中使用Ingress互相通讯时比较常见,里面的ip大多都是某个内部域名。
- 原因:转发前会重新拼装新的url,这个拼装逻辑在
org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter#filter,其中代码如下:
1 Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR); 2 if (route == null) { 3 return chain.filter(exchange); 4 } else { 5 log.trace("RouteToRequestUrlFilter start"); 6 URI uri = exchange.getRequest().getURI(); 7 boolean encoded = ServerWebExchangeUtils.containsEncodedParts(uri); 8 URI routeUri = route.getUri(); 9 if (hasAnotherScheme(routeUri)) { 10 exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme()); 11 routeUri = URI.create(routeUri.getSchemeSpecificPart()); 12 } 13 14 if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) { 15 throw new IllegalStateException("Invalid host: " + routeUri.toString()); 16 } else { 17 URI mergedUrl = UriComponentsBuilder.fromUri(uri).scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri(); 18 exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl); 19 return chain.filter(exchange); 20 } 21 }
我们单挑这一句URI mergedUrl = UriComponentsBuilder.fromUri(uri).scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri();来看,其实它只帮你拼装了你的scheme(协议,如http),host(域名或者ip),port(端口号),并没有你配置的path
- 解决方案:
1spring: 2 cloud: 3 gateway: 4 routes: 5 - id: routeUser 6 uri: http://192.168.1.1:8081/dev/yyy 7 predicates: 8 - Path=/xxx/user/** 9 filters: 10 # StripPrefix:去除原始请求路径中的前1级路径 11 - StripPrefix=1 12 # 对于/xxx/user开头的url转发时拼装新的url的path前都添加一个/dev/yyy前缀 13 # 也可以进行rewritePath,不过不如当前方案简洁 14 - PrefixPath=/dev/yyy
坑点5 自定义全局过滤器获取的requestPath非原始的requestPath
- 问题描述:我的项目有在gateway中做登录校验,但有一些需要不需要登录就可以访问的url会在校验前进行验证白名单,判断这些url我使用的是
exchange.getRequest().getURI().getPath(),因为坑点4的配置,当我请求localhost:8080/xxx/user/login我拿到的path为/dev/yyyy/login,而我期望的是拿到/xxx/user/login - 原因:
routeUser中的filter走完后,请求path就已经改完了,我的全局过滤器是在route之后的 - 解决方案:
1LinkedHashSet<URI> uriLinkedHashSet = (LinkedHashSet<URI>)exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR); 2String requestUrlPath = uriLinkedHashSet.iterator().next().getPath();
exchange.getAttributes()中的ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR存放了原始的path,直接取出即可。
坑点6 gateway中使用feign 报 block()/blockFirst()/blockLast() are blocking, which is not supported in thread reactor-http-nio-x
feign的uri使用域名或者url负载均衡没啥问题,但是使用服务名负载均衡就会报以上错误。 查了下资料,原因是loadbalancer中没有非阻塞的client,高版本的loadbalancer把唯一一个支持异步的ribbon组件去掉了,于是要自己配置下非阻塞的客户端。
1import org.springframework.cloud.client.ServiceInstance; 2import org.springframework.cloud.client.loadbalancer.Request; 3import org.springframework.cloud.client.loadbalancer.Response; 4import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer; 5import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient; 6import reactor.core.publisher.Mono; 7 8import java.util.concurrent.CompletableFuture; 9import java.util.concurrent.ExecutionException; 10 11public class CustomNonBlockingLoadBalancerClient extends BlockingLoadBalancerClient { 12 13 private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory; 14 15 public CustomNonBlockingLoadBalancerClient(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory) { 16 super(loadBalancerClientFactory); 17 this.loadBalancerClientFactory = loadBalancerClientFactory; 18 } 19 20 @Override 21 public <T> ServiceInstance choose(String serviceId, Request<T> request) { 22 ReactiveLoadBalancer<ServiceInstance> loadBalancer = 23 loadBalancerClientFactory.getInstance(serviceId); 24 if (loadBalancer == null) { 25 return null; 26 } 27 CompletableFuture<Response<ServiceInstance>> f = 28 CompletableFuture.supplyAsync(() -> { 29 Response<ServiceInstance> loadBalancerResponse = 30 Mono.from(loadBalancer.choose(request)).block(); 31 return loadBalancerResponse; 32 }); 33 Response<ServiceInstance> loadBalancerResponse = null; 34 try { 35 loadBalancerResponse = f.get(); 36 } catch (InterruptedException e) { 37 e.printStackTrace(); 38 } catch (ExecutionException e) { 39 e.printStackTrace(); 40 } 41 if (loadBalancerResponse == null) { 42 return null; 43 } 44 return loadBalancerResponse.getServer(); 45 } 46}
配置以上客户端替换默认的:
1import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; 2import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; 3import org.springframework.context.annotation.Bean; 4import org.springframework.context.annotation.Configuration; 5 6import javax.annotation.Resource; 7 8@Configuration 9public class LoadBalancerClientConfig { 10 11 @Resource 12 private LoadBalancerClientFactory loadBalancerClientFactory; 13 14 @Bean 15 public LoadBalancerClient blockingLoadBalancerClient() { 16 return new CustomNonBlockingLoadBalancerClient(loadBalancerClientFactory); 17 } 18 19}