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

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

    • 迁移前的准备工作
    • 迁移步骤详解
      • 第一步:查看源码
      • 第二步:启动类迁移
      • 第三步:引入 Gateway 依赖
      • 第四步 编写bootstrap.yaml
      • 第五步:替换路由配置
      • 第六步:迁移过滤器逻辑
      • 第七步:测试与调优
    • 迁移过程中常见问题及解决方案
    • 真实问题:
      • **注意事项:Nginx 转发配置的调整**
        • **问题背景**
        • **解决方法**
    • 总结

公司的项目之前使用的是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 启动类:

@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 启动类:

@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 依赖:

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

<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,所以要在这里说明的。

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 配置:

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 配置:

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 过滤器:


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 全局过滤器:


@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 配置
    原有配置通常类似以下形式:

        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 的地址替换为服务名,示例如下:
  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 的新特性来优化系统架构。

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/934305.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

网站中的QQ在线客服接入

1. 开通QQ通讯组件 QQ通讯组件官网&#xff1a;https://shang.qq.com 默认未开通通讯组件&#xff0c;登陆上QQ之后会提示开通&#xff0c;点击开通即可 2. 唤起QQ临时会话&#xff08;对方不是自己的QQ好友也能唤起&#xff09; 复制链接地址 http://wpa.qq.com/msgrd?v3&…

赋能加速AI应用交付,F5 BIG-IP Next for Kubernetes方案解读

随着AI工作负载的爆炸式增长&#xff0c;服务提供商和企业需要加速计算&#xff0c;以安全高效地在大规模云上交付高性能的AI应用。前段时间&#xff0c;F5公司宣布推出一项全新的创新AI应用交付和应用安全解决方案&#xff0c;即BIG-IP Next for Kubernetes。那么该方案有何性…

域内DNS信息收集

目录 一、查询域内DNS记录 1. 使用 PowerView.ps1 2. 使用 adidnsdump 二、添加域内DNS记录 1. 使用 Invoke-DNSUpdate.ps1 在默认情况下,域内所有用户 都有权限读取 Active Directory 数据库中的 DNS 信息,包括所有记录。这是因为: DNS 记录被视为公共信息,用于解析域…

Odoo :一款免费且开源的食品生鲜领域ERP管理系统

文 / 贝思纳斯 Odoo金牌合作伙伴 引言 提供业财人资税的精益化管理&#xff0c;实现研产供销的融通、食品安全的追踪与溯源&#xff0c;达成渠道的扁平化以及直面消费者的 D2C 等数字化解决方案&#xff0c;以此提升运营效率与核心竞争力&#xff0c;支撑高质量的变速扩张。…

在星闪W63/W63E开发板上运行第一个OpenHarmony程序

目录 引言 demolink示例 程序修改 修改任务堆栈的大小 修改示例程序的build.gn 修改App的build.gn 修改ohos.cmake 修改config.py 编译程序 烧写程序 程序运行 结语 引言 在前面的博文星闪WS63E开发板的OpenHarmony环境构建-CSDN博客中介绍了如何构建W63E开发板的…

Spring——@Autowired和@Configuration注解区别

摘要 本文主要介绍了Spring框架中Autowired和Configuration注解的区别。Autowired用于自动注入依赖&#xff0c;支持属性、构造器和方法注入。Configuration则用于定义配置类&#xff0c;允许在类中使用Bean注解声明Bean。文章详细解释了这两个注解的作用、使用场景和核心特性…

机器学习--张量

机器学习–张量 机器学习的数据结构–张量 张量是机器学习程序中的数字容器&#xff0c;本质上就是各种不同维度的数组&#xff0c;如下图所示。 张量的维度称为轴&#xff08;axis&#xff09;&#xff0c;轴的个数称为阶&#xff08;rank&#xff09; 标量–0D张量 impor…

标记数据集生成模型助力无数据情况下的大模型指令微调

在构建大模型应用时&#xff0c;通常有两种方式来改进效果&#xff0c;一种是构建外部知识库&#xff0c;利用RAG来完成。但RAG并不是万能的&#xff0c;对于特定领域的LLM应用&#xff0c;以及无需示例&#xff0c;就能完成特定任务等场合就需要进行微调。然而&#xff0c;微调…

nvm安装指定版本显示不存在及nvm ls-remote 列表只出现 iojs 而没有 node.js 解决办法

在使用 nvm install 18.20.3 安装 node 时会发现一直显示不存在此版本 Version 18.20.3 not found - try nvm ls-remote to browse available versions.使用 nvm ls-remote 查看可安装列表时发现&#xff0c;列表中只有 iojs 解决方法&#xff1a; 可以使用以下命令查看可安装…

5.ABAP结构体和内表

总学习目录请点击下面连接 SAP ABAP开发从0到入职&#xff0c;冷冬备战-CSDN博客 目录 5.1.结构化数据对象 定义 如何引用结构化的数据对象 拷贝 实战练习 创建 拷贝 调试代码 5.2.内表 行类型 键 表种类 存取类型 表类型 如何在本地定义表类型 内表三种可能的…

C#搭建WebApi服务

1&#xff0c;OWIN的介绍 OWIN 的全称是 “Open Web Interface for .NET”&#xff0c; OWIN 在 .NET Web 服务器和 .NET Web 应用之间定义了一套标准的接口&#xff0c; 其目的是为了实现服务器与应用之间的解耦&#xff0c;使得便携式 .NET Web 应用以及跨平台的愿望成为现实…

unity 2D像素种田游戏学习记录(自用)

一、透明度排序轴 改变sprite的排序方式&#xff0c;默认按照z轴进行排序&#xff08;离摄像机的远近&#xff09;。可以将其改变成y轴的排序方式&#xff0c;这样可以使2D人物走在草丛的下方就不被遮挡&#xff0c;走在草丛上方就被遮挡&#xff0c;如下图。 在项目设置-图形…

K8S服务突然中断无法访问:报The node had condition: [DiskPressure]异常

一、背景 程序在运行过程中&#xff0c;突然无法访问&#xff0c;发现后台接口也无法访问&#xff1b;查看kuboard&#xff0c;发现报如下异常&#xff1a;The node had condition: [DiskPressure]. 继续查看磁盘使用率&#xff0c;发现系统盘使用率已经高达93%。问题前后呼应…

VLDB 2024 | 时空数据(Spatial-temporal)论文总结

VLDB 2024于2024年8月26号-8月30号在中国广州举行。 本文总结了VLDB 2024有关时空数据&#xff08;time series data&#xff09;的相关论文&#xff0c;主要包含如有疏漏&#xff0c;欢迎大家补充。 &#x1f31f;【紧跟前沿】“时空探索之旅”与你一起探索时空奥秘&#xf…

GTC2024 回顾 | 优阅达携手 HubSpot 亮相上海,赋能企业数字营销与全球业务增长

从初创企业入门到成长型企业拓展&#xff0c;再到 AI 驱动智能化运营&#xff0c;HubSpot 为企业的每步成长提供了全方位支持。 2024 年 11 月下旬&#xff0c;备受瞩目的 GTC2024 全球流量大会&#xff08;上海&#xff09;成功举办。本次大会汇聚了全国内多家跨境出海领域企业…

如何使用go语言的gin库来搭建一个属于自己的网页版GPT

我们将会使用go语言的gin库来搭建一个属于自己的网页版GPT 一、准备工作 我们需要使用到ollama&#xff0c;如何下载和使用[ollama](Ollama完整教程&#xff1a;本地LLM管理、WebUI对话、Python/Java客户端API应用 - 老牛啊 - 博客园)请看这个文档 有过gin环境的直接运行就可…

用户登录流程详解

目录 前言1. 登录请求的发起1.1 表单设计与数据收集1.2 请求发送与状态反馈 2. 验证码校验2.1 验证码的生成与展示2.2 验证码的校验机制 3. 登录前置校验3.1 检查账户状态3.2 登录频率限制 4. SS认证管理器的用户校验4.1 密码校验机制4.2 用户角色与权限检查 5. 登录成功后的处…

虚拟机与Xshell5和Xftp4连接与虚拟机克隆

虚拟机与Xshell5和Xftp4连接与虚拟机克隆 虚拟机与Xshell5和Xftp4连接 虚拟机与Xshell5连接 下载Xshell5后启动出现如下界面&#xff0c;点击新建 新建会话输入虚拟机命名&#xff0c;如master&#xff0c;主机输入虚拟机IP&#xff0c;xxx.xxx.xxx.xxx然后确认&#xff0c;…

【大模型系列篇】LLaMA-Factory大模型微调实践 - 从零开始

前一次我们使用了NVIDIA TensorRT-LLM 大模型推理框架对智谱chatglm3-6b模型格式进行了转换和量化压缩&#xff0c;并成功部署了推理服务&#xff0c;有兴趣的同学可以翻阅《NVIDIA TensorRT-LLM 大模型推理框架实践》&#xff0c;今天我们来实践如何通过LLaMA-Factory对大模型…

最大值和最小值的差

最大值和最小值的差 C语言代码C 语言代码Java语言代码Python语言代码 &#x1f490;The Begin&#x1f490;点点关注&#xff0c;收藏不迷路&#x1f490; 输出一个整数序列中最大的数和最小的数的差。 输入 第一行为M&#xff0c;表示整数个数&#xff0c;整数个数不会大于1…