Zuul迁移至Spring Cloud Gateway踩坑记录

那年烟雨落申城
• 阅读 289

缘起

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();
}

}

配置以上客户端替换默认的:
```java
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);
    }

}
点赞
收藏
评论区
推荐文章
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
Wesley13 Wesley13
2年前
Java日期时间API系列31
  时间戳是指格林威治时间1970年01月01日00时00分00秒起至现在的总毫秒数,是所有时间的基础,其他时间可以通过时间戳转换得到。Java中本来已经有相关获取时间戳的方法,Java8后增加新的类Instant等专用于处理时间戳问题。 1获取时间戳的方法和性能对比1.1获取时间戳方法Java8以前
Wesley13 Wesley13
2年前
Java并发源码之ReentrantLock
ReentrantLock介绍ReentrantLock是一个可重入的互斥锁,与使用synchronized方法和语句访问的隐式监视锁具有相同的基本行为和语义,但具有扩展功能。ReentrantLock属于最后一个成功加锁并且还没有释放锁的线程。当一个线程请求lock时,如果锁不属于任何线程,将立马得到这个锁;如果锁已经被
Stella981 Stella981
2年前
Kerberos无约束委派的攻击和防御
 0x00前言简介当ActiveDirectory首次与Windows2000Server一起发布时,Microsoft就提供了一种简单的机制来支持用户通过Kerberos对Web服务器进行身份验证并需要授权用户更新后端数据库服务器上的记录的方案。这通常被称为Kerberosdoublehopissue(双跃点问题),
Wesley13 Wesley13
2年前
03.Android崩溃Crash库之ExceptionHandler分析
目录总结00.异常处理几个常用api01.UncaughtExceptionHandler02.Java线程处理异常分析03.Android中线程处理异常分析04.为何使用setDefaultUncaughtExceptionHandler前沿上一篇整体介绍了crash崩溃
Stella981 Stella981
2年前
Noark入门之线程模型
0x00单线程多进程单线程与单进程多线程的目的都是想尽可能的利用CPU,减少CPU的空闲时间,特别是多核环境,今天咱不做深度解读,跳过...0x01线程池锁最早的一部分游戏服务器是采用线程池的方式来处理玩家的业务请求,以达最大限度的利用多核优势来提高处理业务能力。但线程池同时也带来了并发问题,为了解决同一玩家多个业务请求不被
Wesley13 Wesley13
2年前
NEO从源码分析看网络通信
_0x00前言_NEO被称为中国版的Ethereum,支持C和java开发,并且在社区的努力下已经把SDK拓展到了js,python等编程环境,所以进行NEO开发的话是没有太大语言障碍的。比特币在解决拜占庭错误这个问题时除了引入了区块链这个重要的概念之外,还引入了工作量证明(PoW)这个机智的解决方案,通过数学意义上的难题来保证每个
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
京东云开发者 京东云开发者
8个月前
Java服务总在半夜挂,背后的真相竟然是... | 京东云技术团队
最近有用户反馈测试环境Java服务总在凌晨00:00左右挂掉,用户反馈Java服务没有定时任务,也没有流量突增的情况,Jvm配置也合理,莫名其妙就挂了
京东云开发者 京东云开发者
7个月前
ThreadPoolExecutor线程池内部处理浅析 | 京东物流技术团队
我们知道如果程序中并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束时,会因为频繁创建线程而大大降低系统的效率,因此出现了线程池的使用方式,它可以提前创建好线程来执行任务。本文主要通过java的ThreadPoolExecutor来查看线程池