Zuul迁移至Spring Cloud Gateway踩坑记录

缘起

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

  • 日志输出:
Exception in thread "main" java.lang.NoClassDefFoundError: org/springframework/core/metrics/ApplicationStartup
	at org.springframework.boot.SpringApplication.<init>(SpringApplication.java:232)
	at org.springframework.boot.SpringApplication.<init>(SpringApplication.java:245)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1317)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1306)
	at com.example.gateway.GatewayApplication.main(GatewayApplication.java:12)
  • 原因:netty本身版本不统一,netty和reactor版本不统一导致,因为我使用的是公司统一的父pom,并不是直接依赖的spring-boot-parent。 我测试过,使用spring-boot-parent 可以正常启动,无此错误
  • 解决方案:引入bom(请替换成你自己的版本号):
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-framework-bom</artifactId>
    <version>${spring-framework.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>
<dependency>
    <groupId>io.netty</groupId>
    <artifactId>netty-bom</artifactId>
    <version>${netty.version}</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

坑点2 java.lang.ClassNotFoundException: javax.servlet.Filter

  • 日志输出:
Caused by: java.lang.NoClassDefFoundError: javax/servlet/Filter
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.ClassLoader.defineClass1(Native Method)
	at java.lang.ClassLoader.defineClass(ClassLoader.java:763)
	at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
	at java.net.URLClassLoader.defineClass(URLClassLoader.java:467)
	at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:368)
	at java.net.URLClassLoader$1.run(URLClassLoader.java:362)
	at java.security.AccessController.doPrivileged(Native Method)
	at java.net.URLClassLoader.findClass(URLClassLoader.java:361)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	at java.lang.Class.getDeclaredMethods0(Native Method)
	at java.lang.Class.privateGetDeclaredMethods(Class.java:2701)
	at java.lang.Class.getDeclaredMethods(Class.java:1975)
	at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:467)
	... 21 common frames omitted
Caused by: java.lang.ClassNotFoundException: javax.servlet.Filter
	at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
	at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
	at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
	... 61 common frames omitted
  • 原因:这个应该是Spring Cloud Gateway使用的是Spring Webflux而非Spring Web,导致javax包没有引入
  • 解决方案:引入以下jar包
<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>javax.servlet-api</artifactId>
	<version>${your-version}</version>
</dependency>

坑点3 server.servlet.contextPath不生效问题

  • 原因:Spring Cloud Gateway使用的是Spring Webflux,不是Spring Web,所以Spring Web的配置它无法解析
  • 解决方案
    • 方案1. 添加配置:
    spring:
      webflux:
        base-dir: /xxx
    
    • 方案2. 配置routor转发
    spring:
      cloud:
        gateway:
          routes:
            - id: self
              uri: http://localhost:8080
              predicates:
                - Path=/xxx/**
              filters:
                 # StripPrefix=1:去除原始请求路径中的前1级路径,去除2级的话StripPrefix=2
                - StripPrefix=1
    
    • 方案3. 配置默认filter:
     spring:
        cloud:
          gateway:
            default-filters:
              - StripPrefix=1
    

坑点4. spring.cloud.gateway.routes[0].uri带path无效

  • 问题描述,配置如下(当前服务配置了spring.webflux.base-dir=/xxx):
spring:
  cloud:
    gateway:
      routes:
        - id: routeUser
          uri: http://192.168.1.1:8081/dev/yyy
          predicates:
            - Path=/xxx/user/**
          filters:
            # StripPrefix:去除原始请求路径中的前1级路径
            - 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,其中代码如下:
 Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
        if (route == null) {
    
    
            return chain.filter(exchange);
        } else {
    
    
            log.trace("RouteToRequestUrlFilter start");
            URI uri = exchange.getRequest().getURI();
            boolean encoded = ServerWebExchangeUtils.containsEncodedParts(uri);
            URI routeUri = route.getUri();
            if (hasAnotherScheme(routeUri)) {
    
    
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_SCHEME_PREFIX_ATTR, routeUri.getScheme());
                routeUri = URI.create(routeUri.getSchemeSpecificPart());
            }

            if ("lb".equalsIgnoreCase(routeUri.getScheme()) && routeUri.getHost() == null) {
    
    
                throw new IllegalStateException("Invalid host: " + routeUri.toString());
            } else {
    
    
                URI mergedUrl = UriComponentsBuilder.fromUri(uri).scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri();
                exchange.getAttributes().put(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR, mergedUrl);
                return chain.filter(exchange);
            }
        }

我们单挑这一句URI mergedUrl = UriComponentsBuilder.fromUri(uri).scheme(routeUri.getScheme()).host(routeUri.getHost()).port(routeUri.getPort()).build(encoded).toUri();来看,其实它只帮你拼装了你的scheme(协议,如http),host(域名或者ip),port(端口号),并没有你配置的path

  • 解决方案:
spring:
  cloud:
    gateway:
      routes:
        - id: routeUser
          uri: http://192.168.1.1:8081/dev/yyy
          predicates:
            - Path=/xxx/user/**
          filters:
            # StripPrefix:去除原始请求路径中的前1级路径
            - StripPrefix=1
            # 对于/xxx/user开头的url转发时拼装新的url的path前都添加一个/dev/yyy前缀
            # 也可以进行rewritePath,不过不如当前方案简洁
            - 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之后的
  • 解决方案:
LinkedHashSet<URI> uriLinkedHashSet  = (LinkedHashSet<URI>)exchange.getAttributes().get(ServerWebExchangeUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR);
String 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组件去掉了,于是要自己配置下非阻塞的客户端。

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.client.loadbalancer.reactive.ReactiveLoadBalancer;
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
import reactor.core.publisher.Mono;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

public class CustomNonBlockingLoadBalancerClient extends BlockingLoadBalancerClient {
    
    

    private final ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory;

    public CustomNonBlockingLoadBalancerClient(ReactiveLoadBalancer.Factory<ServiceInstance> loadBalancerClientFactory) {
    
    
        super(loadBalancerClientFactory);
        this.loadBalancerClientFactory = loadBalancerClientFactory;
    }

    @Override
    public <T> ServiceInstance choose(String serviceId, Request<T> request) {
    
    
        ReactiveLoadBalancer<ServiceInstance> loadBalancer =
                loadBalancerClientFactory.getInstance(serviceId);
        if (loadBalancer == null) {
    
    
            return null;
        }
        CompletableFuture<Response<ServiceInstance>> f =
                CompletableFuture.supplyAsync(() -> {
    
    
                    Response<ServiceInstance> loadBalancerResponse =
                            Mono.from(loadBalancer.choose(request)).block();
                    return loadBalancerResponse;
                });
        Response<ServiceInstance> loadBalancerResponse = null;
        try {
    
    
            loadBalancerResponse = f.get();
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        } catch (ExecutionException e) {
    
    
            e.printStackTrace();
        }
        if (loadBalancerResponse == null) {
    
    
            return null;
        }
        return loadBalancerResponse.getServer();
    }
}

配置以上客户端替换默认的:

import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.annotation.Resource;

@Configuration
public class LoadBalancerClientConfig {
    
    

    @Resource
    private LoadBalancerClientFactory loadBalancerClientFactory;

    @Bean
    public LoadBalancerClient blockingLoadBalancerClient() {
    
    
        return new CustomNonBlockingLoadBalancerClient(loadBalancerClientFactory);
    }

}

猜你喜欢

转载自blog.csdn.net/u013014691/article/details/128093298