从 Zuul 迁移到 Spring Cloud Gateway:一步步实现服务网关的升级

从 Zuul 迁移到 Spring Cloud Gateway:一步步实现服务网关的升级

公司的项目之前使用的是Zuul,然后使用的是以前传下来的jar包,JDK1.8,spring1.*,都是比较老了,然后因为这些原因,要把Zuul替换成Gateway。

本文将详细介绍如何从 Zuul 迁移到 Gateway。

迁移前的准备工作

在开始迁移之前,需要做好以下准备:

  1. 确认现有的 Zuul 配置

    收集 Zuul 的路由配置、过滤器逻辑和插件依赖。

  2. 学习 Gateway 的基本概念

    熟悉 Gateway 的核心概念,例如:

    • Route(路由)
    • Predicate(断言)
    • Filter(过滤器)
  3. 确保系统支持响应式编程模型

    检查项目中的依赖库和代码是否与 Spring WebFlux 的非阻塞模型兼容。

  4. 升级到支持 Gateway 的 Spring Boot 版本

    确保 Spring Boot 版本 >= 2.1。


迁移步骤详解

第一步:查看源码

由于项目使用的是预先打包好的 Jar 文件,源码不可直接查看,因此需要通过反编译工具提取代码。我使用的是 jd-gui 工具,界面如图所示:

从反编译的结果可以看到,代码量相对简单,主要包含两个部分:启动类和核心过滤器。相对比较容易。

第二步:启动类迁移

原 Zuul 启动类:

java 复制代码
@EnableZuulProxy
@SpringBootApplication
public class ZuulServerApplication {
    public static void main(String[] args) {
        (new SpringApplicationBuilder(ZuulServerApplication.class))
                .web(true)
                .run(args);
    }

    @Bean
    public PathRewriteHeaderFilter customAddHeaderFilter(RouteLocator routeLocator) {
        return new PathRewriteHeaderFilter(routeLocator);
    }
}

迁移后的 Gateway 启动类:

java 复制代码
@SpringBootApplication
@EnableDiscoveryClient
@ComponentScan(basePackages = {"com.aspire.gateway.gatewayservice"})
public class GatewayServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(GatewayServiceApplication.class, args);
    }
}
  • Spring Boot 2.x 后,@EnableZuulProxy 不再需要,Gateway 默认支持路由功能。
  • 由于项目的特殊需求,需要添加 @ComponentScan 手动指定 Bean 扫描路径,确保组件能够被正确加载。
  • 因为spring2之后的版本不需要再显示指定Gateway了,其实理论上只需要一个SpringBootApplication就够了,其他其实都不用。但是我这里不知道为啥,扫描不到我的bean,所以我就写了扫描当前启动类。@ComponentScan(basePackages = {"com.aspire.gateway.gatewayservice"})这里你可以换成自己的扫描包路径。

第三步:引入 Gateway 依赖

pom.xml 中移除 Zuul 相关依赖,替换为 Gateway 依赖:

以下是我使用的版本控制,就是这些版本之间是兼容的,我使用的也是这些版本。

xml 复制代码
<properties>
        <java.version>17</java.version>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <spring-cloud.version>2024.0.0</spring-cloud.version> <!-- Spring Cloud 2024.x -->
        <spring-cloud-alibaba.version>2022.0.0.0-RC2</spring-cloud-alibaba.version> <!-- Spring Cloud Alibaba 对应版本 -->
        <keycloak.version>22.0.4</keycloak.version>   <!-- 非必须,我的项目需要,你不用就删掉 -->
    </properties>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

这一步主要就是导入你的依赖嘛。

第四步 编写bootstrap.yaml

这一块里面其实主要就是你的nacos的配置文件,反正我用的是nacos,因为Zuul是网关嘛,Gateway也是网关,然后你实际的服务和网关都是要在同一个服务发现下面的,我之前是eureka,现在是nacos,所以要在这里说明的。

yaml 复制代码
spring:
  main:
    allow-circular-references: true
    allow-bean-definition-overriding: true
  application:
    name: rbac-gateway
  cloud:
    nacos:
      username: ${ENV_CONFIG_USERNAME:nacos}
      password: ${ENV_CONFIG_PASSWORD:}
      server-addr: ${ENV_CONFIG_IP:10.*.*.*}:${ENV_CONFIG_PORT:*}

      # Nacos 服务发现配置
      discovery:
        enabled: true  # 启用服务发现
        service: ${spring.application.name}  # 使用应用名作为服务名
        server-addr: ${ENV_CONFIG_IP:*}:${ENV_CONFIG_PORT:*}
        namespace: ${NAMESPACE:*}
        #group: ${spring.cloud.nacos.discovery.group:*}
        group: *
        metadata:
          version: v1
          env: prod

    # Nacos 配置中心配置
      config:
        enabled: true
        server-addr: ${ENV_CONFIG_IP:*}:${ENV_CONFIG_PORT:*}
#        group: ${spring.cloud.nacos.discovery.group:*}
        group: *
        namespace: ${NAMESPACE:*}
        file-extension: yml
        shared-configs:
          - data-id: ${CONFIG_DATA_ID:ms-gateway.yml}
            group: *
            refresh: true
      timeout: 600000
      config-long-poll-timeout: 5000
      config-retry-time: 2000
      max-retry: 3
      refresh-enabled: true

第五步:替换路由配置

将 Zuul 的 application.yml 配置迁移为 Gateway 的路由配置。这一块实际上就比较复杂了,因为他们之间的切换还是很麻烦的,所以我这里是直接使用AI帮我替换的,你也可以这样。

反正差不多样子就是如下吧。直接让AI帮你替换,然后你看一眼就行了。我反正是这么搞的,然后也没啥问题。

Zuul 配置:

yaml 复制代码
zuul:
  #
  semaphore:
    max-semaphores: 1000
  servlet-path: /
  host:
    connect-timeout-millis: 60000
    socket-timeout-millis: 60000
  #
  routes:
    smartdata-check:
      path: /smartCheck
      service-id: rbac
      strip-prefix: false
    smartdata-token-init:
      path: /v1/smartdata/token
      service-id: rbac
      strip-prefix: false
    composite-roles:
      path: /v1/roles/**
      service-id: rbac
      strip-prefix: false

Gateway 配置:

yaml 复制代码
spring:
  cloud:
    gateway:
      routes:
        - id: sso
          uri: lb://rbac
          predicates:
            - Path=/v1/alerts/sso/**
        - id: smartdata-check
          uri: lb://rbac
          predicates:
            - Path=/smartCheck
        - id: smartdata-token-init
          uri: lb://rbac
          predicates:
            - Path=/v1/smartdata/token
        - id: composite-roles
          uri: lb://rbac
          predicates:
            - Path=/v1/roles/**

其实没有全局过滤器,已经可以用了,就是网关服务已经是可以用了。到这里其实就已经结束了。服务能用。不看后面也行,我为什么要替换呢,因为我想完美迁移。

第六步:迁移过滤器逻辑

Zuul 使用过滤器机制来处理请求,而 Gateway 则使用过滤器工厂。这一块就比较复杂了,也是我花的最多时间的一步了。

原本的Zuul 过滤器:

java 复制代码
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.representations.AccessToken;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.Route;
import org.springframework.cloud.netflix.zuul.filters.RouteLocator;
import org.springframework.web.util.UrlPathHelper;

public class PathRewriteHeaderFilter extends ZuulFilter {
  private static final Logger log = LoggerFactory.getLogger(com.migu.tsg.microservice.zuul.PathRewriteHeaderFilter.class);
  
  private RouteLocator routeLocator;
  
  private final UrlPathHelper urlPathHelper = new UrlPathHelper();
  
  private static final String EMPLOYEE_TYPE = "employeeType";
  
  private static final String ORG_ACCOUNT = "head_orgAccount";
  
  private static final String IS_ADMIN = "head_isAdmin";
  
  private static final String IS_SUPERUSER = "head_isSuperUser";
  
  private static final String USER_NAME = "head_userName";
  
  private static final String FALSE = "false";
  
  private static final String TRUE = "true";
  
  private static final String ADMIN = "admin";
  
  private static final String ROOT = "root";
  
  private static final Integer SIX = Integer.valueOf(6);
  
  private static final String COLON = ":";
  
  public PathRewriteHeaderFilter() {}
  
  public PathRewriteHeaderFilter(RouteLocator routeLocator) {
    this.routeLocator = routeLocator;
  }
  
  public int filterOrder() {
    return SIX.intValue();
  }
  
  public String filterType() {
    return "pre";
  }
  
  public boolean shouldFilter() {
    return true;
  }
  
  public Object run() {
    RequestContext requestContext = RequestContext.getCurrentContext();
    String requestURI = this.urlPathHelper.getPathWithinApplication(requestContext.getRequest());
    Route route = this.routeLocator.getMatchingRoute(requestURI);
    try {
      if (route != null) {
        String location = route.getLocation();
        log.info("location: {}", location);
        if (location != null) {
          HttpServletRequest request = requestContext.getRequest();
          KeycloakSecurityContext securityContext = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
          handleRewriteHeader(securityContext, requestContext);
          if (location.startsWith("http:") || location.startsWith("https:"))
            log.info("forward url is : " + location); 
        } 
      } 
    } catch (Exception e) {
      requestContext.set("error.status_code", Integer.valueOf(500));
      requestContext.set("error.message", e.getCause());
      requestContext.set("error.exception", e);
    } 
    return null;
  }
  
  private void handleRewriteHeader(KeycloakSecurityContext securityContext, RequestContext requestContext) {
    log.info("keycloak securityContext = {}", securityContext);
    if (securityContext == null)
      return; 
    AccessToken token = securityContext.getToken();
    Map<String, Object> otherClaims = token.getOtherClaims();
    log.info("keycloak token = {}, otherClaims = {}", token, otherClaims);
    String employeeType = (String)otherClaims.get("employeeType");
    String userName = (String)otherClaims.get("userName");
    String orgAccount = "";
    String isSupperUser = "false";
    String isAdmin = "false";
    if (employeeType.equals("root")) {
      isSupperUser = "true";
      orgAccount = userName;
    } 
    if (employeeType.equals("admin")) {
      isAdmin = "true";
      orgAccount = userName;
    } 
    if (!employeeType.equals("root") && !employeeType.equals("admin")) {
      int index = employeeType.indexOf(".") + 1;
      orgAccount = employeeType.substring(index, employeeType.length());
    } 
    log.info("employeeType: {}, head_userName: {}, head_isSuperUser: {}, head_orgAccount: {}, head_isAdmin: {}", new Object[] { employeeType, userName, isSupperUser, orgAccount, isAdmin });
    requestContext.addZuulRequestHeader("head_userName", userName);
    requestContext.addZuulRequestHeader("head_isSuperUser", isSupperUser);
    requestContext.addZuulRequestHeader("head_orgAccount", orgAccount);
    requestContext.addZuulRequestHeader("head_isAdmin", isAdmin);
  }
}

但是我是想完美的等量替换,所以这里就把原本的过滤器也给拿过来了。可以看到是少了一些东西了,因为原本的方法有很多东西是用不到的,我就把那些东西给删掉了。只保留了用到的东西

反正我测下来,是没啥问题,反正就是实现起来差别真的很大,首先是extends ZuulFilter不用了,改成了GlobalFilter ,然后里面的实现也从public Object run() 变成了public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) ,然后具体的实现逻辑也变了,这一块呢,就是自己琢磨着改吧。每一个过滤器都不一样,反正大体逻辑就是实现的方法不一样了,然后重写的方法不一样了。这两个是最主要的。

  1. 实现的接口不一样
  2. 重写的方法不一样

其实主要把握这两个就行,里面就是具体的代码逻辑了。

替换后的Gateway 全局过滤器:

java 复制代码
@Component
@Order(6)  // 这里使用 @Order 注解来设置过滤器顺序
public class PathRewriteHeaderFilter implements GlobalFilter {

    private static final Logger log = LoggerFactory.getLogger(PathRewriteHeaderFilter.class);

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        try {
            String requestURI = exchange.getRequest().getURI().getPath();
            log.info("Request URI: {}", requestURI);

            // 在此处你可以获取并处理 Keycloak 的 securityContext 和 token
            KeycloakSecurityContext securityContext = exchange.getAttribute(KeycloakSecurityContext.class.getName());
            if (securityContext != null) {
                AccessToken token = securityContext.getToken();
                Map<String, Object> otherClaims = token.getOtherClaims();
                log.info("keycloak token = {}, otherClaims = {}", token, otherClaims);

                String employeeType = (String) otherClaims.get("employeeType");
                String userName = (String) otherClaims.get("userName");
                String orgAccount = "";
                String isSuperUser = "false";
                String isAdmin = "false";

                if ("root".equals(employeeType)) {
                    isSuperUser = "true";
                    orgAccount = userName;
                } else if ("admin".equals(employeeType)) {
                    isAdmin = "true";
                    orgAccount = userName;
                } else {
                    int index = employeeType.indexOf(".") + 1;
                    orgAccount = employeeType.substring(index);
                }

                log.info("employeeType: {}, head_userName: {}, head_isSuperUser: {}, head_orgAccount: {}, head_isAdmin: {}",
                        employeeType, userName, isSuperUser, orgAccount, isAdmin);

                exchange.getRequest().mutate()
                        .header("head_userName", userName)
                        .header("head_isSuperUser", isSuperUser)
                        .header("head_orgAccount", orgAccount)
                        .header("head_isAdmin", isAdmin)
                        .build();
            }

        } catch (Exception e) {
            log.error("Error processing request", e);
        }

        return chain.filter(exchange);
    }
}

第七步:测试与调优

  1. 功能测试

    验证迁移后的路由和过滤器逻辑是否正常工作。

    我这里是正常的,没有问题的。

  2. 性能测试

    测试 Gateway 的吞吐量和延迟,确保性能满足要求。

  3. 监控与日志

    配置 Gateway 的监控和日志,及时捕获异常和瓶颈。


迁移过程中常见问题及解决方案

  1. 问题:某些依赖库与 WebFlux 不兼容
    解决方案: 更新相关依赖或寻找替代方案,确保与 WebFlux 模型兼容。

  2. 问题:路由配置规则变更导致服务无法访问
    解决方案: 仔细对比 Zuul 和 Gateway 的配置方式,确保路径匹配规则正确。

  3. 问题:过滤器执行顺序混乱
    解决方案: 合理设置过滤器的 Order 值,并明确其执行逻辑。

真实问题:

注意事项:Nginx 转发配置的调整

在迁移过程中,有一个细节需要特别注意,那就是 Nginx 的转发规则。以下是我遇到的问题和解决方法,希望能对你有所帮助。

问题背景

在原有的 Zuul 部署环境中,我使用的是 IP+端口 的形式进行服务转发。由于迁移初期 IP 和端口并未发生改变,所以 Nginx 的配置无需修改,服务能够正常使用。然而,当将 Gateway 部署到云原生环境(如 Kubernetes)后,问题随之出现。

云原生环境中,服务之间的通信通常使用 服务名:端口 的形式,而不是 IP 地址。因此,原本在 Nginx 中配置的 qams-zuul-server 服务名需要进行修改,否则转发规则无法正确匹配,导致请求失败。


解决方法
  1. 检查原有的 Nginx 配置
    原有配置通常类似以下形式:
r 复制代码
        location ^~/v1/ {
            proxy_pass   http://zuulServer;
        }
         location ^~/v2/ {
            #proxy_pass   http://10.24.88.160:5566;
            proxy_pass   http://10.12.7.115:5566;
        }
         location ^~/zuul/ {
            #proxy_pass   http://10.24.88.160:5566;
            proxy_pass   http://10.12.7.115:5566;
        }
          location ^~/download/ {
            proxy_pass   http://10.24.88.160:2222;
        }

这种配置基于固定的 IP 和端口,在云原生环境下无法适用。

  1. 修改为基于服务名的配置
    在云原生环境中,需要将 10.12.7.115 的地址替换为服务名,示例如下:
cobol 复制代码
  location ^~/v1/ {                                                                                         
        proxy_pass   http://qams-gateway-server:5566;
    }
    
    location ^~/v2/ {                                                                                       
        proxy_pass   http://qams-gateway-server:5566;                                                                 
    } 
    
    location ^~/gateway/ {                                                                                       
        proxy_pass   http://qams-gateway-server:5566;                                                                 
    }
    
    location ^~/download/ {                                                                                       
        proxy_pass   http://qams-gateway-server:2222;                                                                 
    }

注意:

  • 服务名 qams-gateway-service 必须与云原生环境中定义的服务名称一致。
  • 确保 Nginx 能够解析服务名。通常情况下,Nginx 部署在同一 Kubernetes 集群内,DNS 解析应当是自动支持的。
  1. 重启 Nginx 并测试
    完成修改后,重启 Nginx 并通过实际访问测试转发是否正常。

迁移的时候注意nginx的转发,我之前呢,是因为我使用的是IP+端口的形式,然后我的IP和端口实际上并没有发生改变,所以我的Nginx没改然后服务依旧能正常使用。

但是当我把Gateway迁移到云原生环境下的时候,就不太行了,因为云原生环境使用的是服务名:端口的格式,所以他原本的服务名称为:qams-zuul-server,要换成下面的格式,就是nginx也要需要,这个不要忘记了。


总结

从 Zuul 迁移到 Spring Cloud Gateway 是一次提升系统性能和功能的好机会。通过合理规划和逐步迁移,可以平稳完成网关的升级,并充分利用 Gateway 的新特性来优化系统架构。

希望这篇文章能为你的迁移过程提供有价值的参考!如果你在迁移过程中遇到问题,欢迎留言讨论。

相关推荐
深海潜水员29 分钟前
【Python】 切割图集的小脚本
开发语言·python
27669582921 小时前
东方航空 m端 wasm req res分析
java·python·node·wasm·东方航空·东航·东方航空m端
Yolo566Q1 小时前
R语言与作物模型(以DSSAT模型为例)融合应用高级实战技术
开发语言·经验分享·r语言
Felven1 小时前
C. Challenging Cliffs
c语言·开发语言
星月昭铭2 小时前
Spring AI调用Embedding模型返回HTTP 400:Invalid HTTP request received分析处理
人工智能·spring boot·python·spring·ai·embedding
Dreamsi_zh2 小时前
Python爬虫02_Requests实战网页采集器
开发语言·爬虫·python
_君落羽_3 小时前
C语言 —— 指针(4)
c语言·开发语言
weixin_448617053 小时前
疏老师-python训练营-Day30模块和库的导入
开发语言·python
望星空听星语3 小时前
C语言自定义数据类型详解(四)——联合体
c语言·开发语言
壹立科技4 小时前
Java源码构建智能名片小程序
java·开发语言·小程序