一、OpenFeign简介

1、概念

OpenFeign是一个声明式 Web 服务客户端,它使的调用远程服务就像调用本地服务一样简单,只需要创建一个接口并在它上面使用相应的注解(@FeignClient)即可。

2、HTTP和RPC

HTTP是基于请求-响应模式的应用层协议,通信灵活,基于文本(JSON/XML),遵循RESTful风格,广泛应用Web开发等多种场景。
RPC属于OSI模型中会话层的协议,当然现实际上使用的还是TCP/IP模型,所以也算是应用层协议。它的核心思想和OpenFeign是一样的,不过更侧重于高性能的进程间通信,通过IDL定义接口,并支持二进制等高效数据格式,虽底层可复用HTTP等传输协议,但整体设计旨在为分布式系统提供高效、低延迟的调用能力。
至于gRPC,它既是一种框架也定义了自己的协议规范,底层是基于HTTP/2实现的。

3、OpenFeign和Feign的关系

Feign是Netfilx开源的一个轻量级的HTTP客户端模板工具,而OpenFeign 是 Spring Cloud 对原生 Feign 的增强封装,它在保留 Feign核心能力的基础上,深度集成了 Spring 生态,在微服务架构中实现远程调用就像调用本地方法一样简单、高效。简单说,​OpenFeign 就是让 Feign 在 Spring Cloud 微服务环境中“开箱即用”的增强版。从反方向去理解,也就是说OpenFeign要比Feign更重。理论上来讲,OpenFeign更适合微服务,而Feign更适合单体。

4、OpenFeign和Dubbo的区别

OpenFeign基于HTTP协议,采用RESTful风格,深度集成于Spring Cloud生态系统,非常适合构建基于HTTPRESTful微服务,但其本身不直接提供服务治理功能。
Dubbo则是一个高性能的分布式服务框架,核心是基于RPC协议的远程方法调用,并自带完整的服务治理能力(如服务发现、负载均衡、容错机制),拥有独立的生态系统,更适用于构建需要全面服务治理和高性能通信的面向服务架构。
需要注意的是,OpenFeignDubbo并不是互斥的关系。实际上,Dubbo可以使用HTTP协议作为通信方式,而OpenFeign也可以集成RPC协议进行远程调用。选择使用哪种远程调用方式取决于具体的业务需求和技术栈的选择。

5、OpenFeign的工作原理

在这里插入图片描述

OpenFeign 的核心机制围绕两个注解展开:@EnableFeignClients@FeignClient。在应用启动阶段,@EnableFeignClients注解通过 @Import导入了 FeignClientsRegistrar,该类会扫描类路径下被 @FeignClient标注的接口。
对于每一个这样的接口,OpenFeign 会为其生成一个动态代理对象并注册到 Spring 容器中。在这个过程中,关键的一步是为接口中的每个方法解析其上的注解(如 @RequestMapping),并生成一个 MethodMetadata对象。这个元数据对象像一个“蓝图”,预先定义了请求的静态信息,如目标 URL 路径模板、HTTP 方法、参数绑定关系等,但它还不是可执行的 RequestTemplate
当程序调用接口方法时,动态代理会拦截调用。代理内部的 SynchronousMethodHandler会根据“蓝图”(MethodMetadata)实时创建一个新的 RequestTemplate实例,并将本次调用的实际参数值按规则填充进去,从而封装出完整的请求信息。随后,流程交由 FeignBlockingLoadBalancerClient执行,由负载均衡器选择具体实例,将服务名替换为真实 IP 地址,最终delegate(委托)给底层 HTTP 客户端完成网络请求并返回结果。

二、负载均衡简介

1、服务端和客户端的负载均衡

服务端负载均衡是在客户端与服务集群之间部署一个独立的负载均衡器(如Nginx),客户端将所有请求发送到这个统一入口,由负载均衡器根据预设策略(轮询、权重等)将请求转发到后端的某个健康服务实例。这种方式对客户端透明、配置集中,但存在单点故障风险且多一次网络跳转。
客户端负载均衡的实现原理是通过注册中心(Nacos),将可用的服务列表拉取到本地(客户端),再通过客户端负载均衡器(LoadBalancer)选择一个服务(IP+端口),再通过HTTP底层客户端请求服务并得到结果。

2、LoadBalancer和Ribbon

Spring Cloud LoadBalancer 是 Spring 官方为取代 Netflix Ribbon 而推出的新一代客户端负载均衡组件。Ribbon 作为微服务早期的经典解决方案,功能成熟但架构较重、且已进入维护模式。而 LoadBalancer 则采用更轻量的设计,原生支持响应式编程,并与 Spring Cloud 生态深度集成,成为当前以及未来的默认选择。

3、负载均衡算法

1、轮询算法(Round Robin):轮询算法是最简单的负载均衡算法之一。它按照顺序将请求依次分配给每个后端服务器,循环往复。当请求到达时,负载均衡器按照事先定义的顺序选择下一个服务器。轮询算法适用于后端服务器具有相同的处理能力和性能的场景。

2、加权轮询算法(Weighted Round Robin)加权轮询算法轮询算法的基础上增加了权重的概念。每个后端服务器都被赋予一个权重值权重值越高,被选中的概率就越大。这样可以根据服务器的处理能力和性能调整请求的分配比例,使得性能较高的服务器能够处理更多的请求。

3、随机算法(Random)随机算法将请求随机分配给后端服务器。每个后端服务器有相等的被选中概率,没有考虑服务器的实际负载情况。这种算法简单快速,适用于后端服务器性能相近且无需考虑请求处理能力的场景。

4、加权随机算法(Weighted Random)加权随机算法在随机算法的基础上引入了权重的概念。每个后端服务器被赋予一个权重值权重值越高,被选中的概率就越大。这样可以根据服务器的处理能力和性能调整请求的分配比例。

5、最少连接算法(Least Connection):最少连接算法会根据后端服务器当前的连接数来决定请求的分配。负载均衡器会选择当前连接数最少的服务器进行请求分配,以保证后端服务器的负载均衡。这种算法适用于后端服务器的处理能力不同或者请求的处理时间不同的场景。

6、哈希算法(Hash):哈希算法会根据请求的某个特定属性(如客户端IP地址、请求URL等)计算哈希值,然后根据哈希值选择相应的后端服务器。

三、开始使用

OpenFeign是客户端负载均衡的,依赖和配置都应该放在服务消费端,而不是服务提供端。当然,现在很多服务既是消费也是提供。一般情况下,会把OpenFeign客户端抽离出来,编成一个公共模块,谁要消费,引入即可。

OpenFeign客户端模块

1、引入依赖
        <!-- openfeign 远程调用 -->
        <!--作用域只有compile和runtime时,才会将依赖传递给引入该模块的服务。-->
        <!--虽默认是compile,但可能不会按预期传递,所以还是要显示设置一下。-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <scope>compile</scope>
        </dependency>
         <!-- okhttp 客户端(可选,不需要不用添加) -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
            <scope>compile</scope>
        </dependency>
        <!-- sprign web模块(可选,不需要不用添加) -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>
2、编写OpenFeign客户端接口
package com.zhjn.api.check;

import com.zhjn.entity.check.UserVO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

/**
 * Author: 千秋
 * Date: 2025/09/30/10:02
 * Description:  OpenFeign客户端接口
 * 注意事项:
 *      1、这是一个OpenFeign声明式客户端的接口,接口上面一定要添加@FeignClient注解。
 *      2、接口名随便定义,但是不要和项目中的类产生冲突了。
 *      通常情况下,有几种写法:
 *          1、业务名(Controller名) + Client(FeignClient)。我就是用这种,简单明了,名字不长,也很好理解,它是OpenFeign客户端的一个声明接口。
 *          2、业务名 + Service。这种说实话,不建议,很容易就会有和业务接口名发生冲突。
 *          3、业务名 + FeignService。
 *      当然具体怎么命名,看团队规范。
 *      3、OpenFeign客户端接口以及接口的请求参数的实体类、返回值实体类,要写在服务消费端里面。
 *      通常写在一个公共的模块中(最好是和公共的基础模块分开),方便多个服务消费端使用的场景。OpenFeign依赖也放这里面,然后在服务消费端引入这个模块就行。
 *          模块名:项目名-common-api
 *          包1:com.项目名.api.服务提供者的简略服务名,然后在这下面放这个服务的所有客户端接口。例:com.zhjn.api.check
 *          包2:com.项目名.entity.服务提供者的简略服务名,然后在这下面放客户端接口所要使用的实体类。例:com.zhjn.entity.check
 */

@FeignClient(name = "check-server", contextId = "TestClient", path = "/api/check/test")
public interface TestClient {
    @PostMapping
    String test(UserVO userVO);
    @GetMapping("/getTest")
    String test2();
}

@FeignClient注解中的属性

属性 例子 作用
name name = “check-server” 指定服务名,注册中心服务提供者的服务名。此属性和value属性,必须要设置一个,否则会出错。
path path= “check-server” 指定请求路径前缀,不填也是ok的,但这样更优雅。
contextId contextId = “TestClient” 指定客户端名称,解决同服务多客户端冲突问题。注1
url url= “http://localhost:8080” 指定服务 URL。和name一起使用,URL优先。
value value= “check-server” value是name的别名,两者完全等价。若只有单个属性,可省略。
configuration configuration= FeignConfiguration.class 指定Feign配置类,通常用来指定局部配置,日志配置、添加请求头等。
fallback fallback= Fallback.class 指定fallback类,也就是熔断降级实现类。注2
fallbackFactory fallbackFactory= FallbackFactory.class 指定fallbackFactory类,也就是熔断降级实现类工厂类。注3
primary primary=true/false 标记默认注入的Bean,通常和qualifiers一起使用。注4
qualifiers qualifiers=“TestClient” 指定客户端限定符,通常和primary一起使用。注5
decode404 qualifiers=“TestClient” 指定404是否解码为null。当 HTTP 响应状态码为 404 时,是否解码为 null而非抛出异常,默认是false。

注1:若未指定,Spring容器启动时,多个 Feign 客户端都会尝试注册名为,服务名.FeignClientSpecification的 Bean,名称冲突,启动失败。

注2:fallback是简单的降级,提供静态的降级处理,无法获取触发降级的异常信息。只适合简单的、不需要知道具体异常的统一降级处理的场景。需要注意的是,需要启用断路器支持,以及熔断降级框架才能够使用。

package com.zhjn.api.check.fallback;

import com.zhjn.api.check.TestClient;
import com.zhjn.api.check.entity.UserVO;
import org.springframework.stereotype.Component;

/**
 * Author: 千秋
 * Date: 2025/10/13/16:16
 * Description:
 *       定义降级类
 */
@Component
public class TestFallback implements TestClient {
    @Override
    public String test(UserVO userVO) {
        return "静态降级数据";
    }
}

注3:fallbackFactory是动态降级,可以获取触发降级的异常信息,实现更精细的降级逻辑。适合需要根据不同的异常类型返回不同的降级结果的场景。需要注意的是,需要启用断路器支持,以及熔断降级框架才能够使用。

package com.zhjn.api.check.fallback;

import com.zhjn.api.check.TestClient;
import feign.FeignException;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

/**
 * Author: 千秋
 * Date: 2025/10/13/16:12
 * Description:  定义降级工厂
 */
@Component
public class TestFallbackFactory implements FallbackFactory<TestClient> {
    @Override
    public TestClient create(Throwable cause) {
        return userVO -> {
            if (cause instanceof FeignException.NotFound) {
                return "没有数据";
            }
            return "服务暂不可用: " + cause.getMessage();

        };
    }
}

注4:primary的作用与Spring 的 @Primary注解完全一致。当 Spring 容器中存在多个相同类型的 Feign 客户端 Bean​ 时,标记 primary = true的客户端会成为默认注入的候选对象,避免因类型冲突导致的 NoUniqueBeanDefinitionException。

注5:qualifiers是一个用于解决多个 Feign 客户端冲突的配置属性,它的作用类似于 Spring 中的 @Qualifier注解。当项目中存在多个 Feign 客户端指向同一个服务​(相同的 name或 url),但需要区分它们时,qualifiers可以明确指定注入的实例。注入时配合Spring 中的 @Qualifier注解,能够精确的控制注入实例。

服务提供方

服务提供方,没什么好说的,直接写Restful 风格的API就行。

package com.zhjn.controller;

import cn.hutool.http.HttpUtil;
import com.zhjn.entity.check.UserVO;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * Author: 千秋
 * Date: 2025/08/28/13:44
 * Description:
 */
@RestController
@RequestMapping("/api/check/test")
public class TestController {

    @Value("${server.port}")
    private String port;
    @PostMapping
    public String test(UserVO userVO) {
        String publicIp = getPublicIp();
        System.out.println("用户信息:"+userVO.toString());
        return "ip"+publicIp+"端口号:"+port+"远程调用成功!";
    }

    public static String getPublicIp() {
        try {
            // 多个API服务备用
            String[] apiUrls = {
                    "https://api.ipify.org",
                    "https://ident.me",
                    "https://checkip.amazonaws.com",
                    "http://whatismyip.akamai.com"
            };
            for (String url : apiUrls) {
                try {
                    String ip = HttpUtil.get(url, 5000).trim();
                    if (ip != null && !ip.isEmpty() && ip.matches("\\d+\\.\\d+\\.\\d+\\.\\d+")) {
                        return ip;
                    }
                } catch (Exception e) {
                    // 尝试下一个API
                    continue;
                }
            }
            return "无法获取公网IP";
        } catch (Exception e) {
            return "获取失败: " + e.getMessage();
        }
    }
}

服务消费者

1、引入依赖
        <!-- Spring Cloud LoadBalancer(负载均衡) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
        <!-- OpenFeign客户端模块-->
        <dependency>
            <groupId>com.zjjnpt.applet</groupId>
            <artifactId>zhyw-common-api</artifactId>
            <version>${zhyw.version}</version>
        </dependency>
2、启用Feign声明式客户端
package com.zhjn;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * Author: 千秋
 * Date: 2025/08/28/14:28
 * Description:
 */
@SpringBootApplication
@EnableDiscoveryClient // 启用SpringCloud的原生注解启动服务注册与发现
@EnableFeignClients    // 启用Feign声明式客户端
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
        System.out.println("-------------------------------------");
        System.out.println("服务启动成功!");
        System.out.println("-------------------------------------");
    }
}
到这里,已经可以使用了。

四、高级特性

前面虽已经介绍了OpenFeign的使用,但是在实际项目中,我们往往需要给API做一些约束优化,如超时重试自定义拦截器(认证传递)、日志等等。

1、日志配置

默认情况下,服务之间的调用是没有日志的。但是可以通过代码或配置文件的来设置日志的级别。

1.1、Spring Boot的日志

Spring Boot默认的日志级别是info,而info是大于debug的。fegin的debug日志就不会输出,所以,需要先把Spring Boot的日志级别调低一些。

#com.zhjn.check是OpenFeign客户端所在的包。
#debug是日志级别。
#也可以在level后面直接设置日志级别,那样的话就是全局的了。
logging:
  level:
    com.zhjn.check: debug    
1.2、OpenFeign日志级别
  1. None:无日志记录(默认)。性能最佳,适合生产环境。
  2. BASIC:仅记录请求方法、URL、响应状态码和执行时间,适用于生产环境追踪问题。
  3. HEADERS:除了BASIC中定义的信息外,还会记录请求和响应的头信息。
  4. FULL:除了HEADERS中定义的信息外,还会记录请求和响应的正文及元数据,适合开发测试环境定位问题。

application.yml属性配置和代码配置,选择其中一个即可。

1.2.1、application.yml属性配置(全局/局部):
spring:
    openfeign:
      #客户端设置
      client:
        config:
          #feignName设置为default是全局的意思。
          #设服务名,就是针对这个服务,例:check-server。
          #也可设置client的contextId,针对客户端,例:TestClient。
          TestClient:
            loggerLevel: full
1.2.2、代码配置(全局/局部):
/*
 * Feign配置类(全局)
 * 如要配置局部的,请把@Configuration去掉。
 * 然后,在客户端@FeignClient注解中的属性configuration,引入该类的Class。
 */
 
@Configuration
public class FeignConfig {

    /**
     * 设置feign日志级别
     *  1. None:无日志记录( 默认)。性能最佳,适合生产环境。
     *  2. BASIC:仅记录请求方法和 URL 以及响应状态代码和执行时间。适用于生产环境追踪问题。
     *  3. HEADERS:记录基本信息以及请求和响应标头。
     *  4. FULL:记录请求和响应的标头、正文和元数据。适合开发测试环境定位问题。
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

2、超时机制

OpenFeign可设置的超时时间有两个:

  1. connect-timeout 连接超时:建立TCP连接的超时时间,从开始尝试连接到成功建立连接的时间。
  2. read-timeout 读取时间:从服务器读取数据的超时时间,指从连接建立成功后,到接收到完整响应的时间,包括服务器处理时间和网络传输时间。

application.yml文件中的属性配置和代码配置,择一即可。

2.1、application.yml属性配置(全局/局部):
spring:
    openfeign:
      #客户端设置
      client:
        config:
          TestClient:
            # 连接超时:建立TCP连接的超时时间,从开始尝试连接到成功建立连接的时间。
            connect-timeout: 2000
            # 读取超时:从连接建立成功后,到接收到完整响应的时间,包括服务器处理时间和网络传输时间。
            read-timeout: 5000
2.2、代码配置(全局/局部):
/*
 * Feign配置类(全局)
 * 如要配置局部的,请把@Configuration去掉。
 * 然后,在客户端@FeignClient注解中的属性configuration,引入该类的Class。
 */
 
@Configuration
public class FeignConfig {

    /**
     * 设置feign超时时间
     * @return
     */
    @Bean
    public Request.Options options() {

        //连接时间:单位是毫秒。默认是10s,不同版本或有差异。
        //建议设置1-5秒之间。TCP的三次握手的时间的非常快的,所以设置短一点。
        int connectTimeoutMillis = 5000;
        //读取时间:单位是毫秒。默认是60s,不同版本或有差异。根据业务需求设置,一般是5-30秒之间。
        int readTimeoutMillis = 5000;
        //接口的总时间 = connectTimeoutMillis + readTimeoutMillis
        return new Request.Options(connectTimeoutMillis, readTimeoutMillis);
    }
}

3、底层HTTP客户端

默认使用的HTTP客户端,是Java自带的HttpURLConnection。此客户端不是池化的,每一次请求都会创建/销毁。在高并发的场景,性能不是很好。可以使用HttpClientOkHttp来替代它。

3.1、application.yml属性配置(全局/局部):
spring:
  cloud:
    openfeign:
      #Http客户端
      okhttp:
        enabled: true

4、重试机制

重试机制,一般情况下是设置在HTTP客户端或者负载均衡选择器上的。但是OpenFeign确实有一个重试机制

重试机制有三个参数:

  1. period:初始重试间隔时间,单位毫秒。(第一次失败后,等待100ms再重试)
  2. maxPeriod:最大重试间隔时间,单位毫秒。(重试间隔会逐渐增加(指数增长),但不会超过 1000ms。100、200、400、800…1000ms)
  3. maxAttempts:最大尝试次数。(总共最多尝试3次,包括第一次调用。也就是重试2次。)
4.1、代码配置(全局/局部):
/*
 * Feign配置类(全局)
 * 如要配置局部的,请把@Configuration去掉。
 * 然后,在客户端@FeignClient注解中的属性configuration,引入该类的Class。
 */
 
@Configuration
public class FeignConfig {

    /**
     * 设置feign重试次数
     * @return
     */
    @Bean
    public Retryer feignRetryer() {
        return new Retryer.Default(100, 1000, 3); 
    }
}

5、自定义拦截器

拦截器,是消费者服务端到消费提供者中间的拦截器,不是Spring MVC的客户端到服务端的拦截器。一般用来添加请求头做token认证传递自定义日志等等。

5.1、编写自定义拦截器:
package com.zhjn.config;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import lombok.extern.slf4j.Slf4j;
import org.apache.logging.log4j.util.Strings;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

/**
 * Author: 千秋
 * Date: 2025/10/28/14:56
 * Description:    自定义Feign拦截器
 */
@Slf4j
public class CustomIntercept implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate requestTemplate) {
        //业务逻辑编写
        //token的请求头。建议写一个枚举类,方便管理。
        String authorization = "Authorization";
        //获取请求头中是否有 token。建议写一个Servlet的工具类,方便获取。
        ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        //获取请求头中token
        String header = requestAttributes.getRequest().getHeader(authorization);
        //如果没有,则使用默认的。
        String token = "123456";
        //设置请求头
        requestTemplate.header("Authorization", Strings.isNotEmpty(header) ? header : token);
    }
}

5.2、应用拦截器:

application.yml文件中的属性配置和代码配置,择一即可。

5.3、application.yml属性配置(全局/局部):
spring:
    openfeign:
      client:
        config:
          TestClient:
            #添加Feign的拦截器 [0]这个下表代表拦截器的顺序
            request-interceptors[0]: com.zhjn.config.CustomIntercept
5.4、代码配置(全局/局部):
/*
 * Feign配置类(全局)
 * 如要配置局部的,请把@Configuration去掉。
 * 然后,在客户端@FeignClient注解中的属性configuration,引入该类的Class。
 */
 
@Configuration
public class FeignConfig {

    /**
     * 自定义feign拦截器
     * @return
     */
    @Bean
    public CustomIntercept customIntercept() {
        return new CustomIntercept();
    }
}

6、负载均衡策略

OpenFeign中,是使用LoadBalancer做负载均衡的(以前是Ribbon)。LoadBalancer默认的负载均衡策略是轮询策略,它还有随机、基于地区、实例偏好、基于提示等等负载均衡策略。也可以设置成跟随nacos的策略,nacos默认也是轮询,但是可以调整实例权重来实现加权随机策略。当然也可以自定义负载均衡策略

LoadBalancer的中文官方文档

6.1、loadBalancer内置策略
  1. 轮询:默认的,不必多说。
  2. 随机:随机算法。
  3. …:太多了,需要的看官方文档吧,设置起来大同小异。
6.1.1、引入依赖

前面我并没有在api公共模块添加loadbalancer,因为openfeign自带了。但是我们这里要更改负载均衡算法,而openfeign里面那个loadbalancer的依赖引入不了,所以这里要添加依赖。

        <!-- Spring Cloud LoadBalancer(负载均衡) -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-loadbalancer</artifactId>
        </dependency>
6.1.2、切换策略的配置类
package com.zhjn.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

/**
 * Author: 千秋
 * Date: 2025/10/30/14:32
 * Description:
 */
public class CustomLoadBalancerConfiguration {

    /**
     * 更换为随机负载均衡算法
     * @param environment
     * @param loadBalancerClientFactory
     * @return
     */
    @Bean
    ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {
        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new RandomLoadBalancer(loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class),
                name);
    }
}

注意:作为 @LoadBalancerClient@LoadBalancerClients 配置参数传递的类,类上不要使用@Configuration 注解,或者在组件扫描范围之外,否则会失效。

6.1.3、在启动类上添加@LoadBalancerClient注解,并指定具体服务或者不指定(全局),引入配置类的Class。也可以写一个配置类(添加了@Configuration注解的),把LoadBalancerClient注解添加到这个类上面,此类内容就是需要此配置的客户端了。
package com.zhjn;

import com.zhjn.config.CustomLoadBalancerConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * Author: 千秋
 * Date: 2025/08/28/14:28
 * Description:
 */
@SpringBootApplication
@EnableDiscoveryClient // 启用SpringCloud的原生注解启动服务注册与发现
@EnableFeignClients    // 启用Feign声明式客户端
//针对某个服务,指定负载均衡策略
//@LoadBalancerClient(name = "check-server", configuration = CustomLoadBalancerConfiguration.class)
//针对所有服务,指定负载均衡策略
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfiguration.class)
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
        System.out.println("-------------------------------------");
        System.out.println("认证授权服务启动成功!");
        System.out.println("-------------------------------------");
    }
}
6.2、使用Nacos的负载均衡策略
6.2.1、application.yml属性配置:
spring:
    #loadBalancer的配置
    loadbalancer:
      nacos:
        enabled: true  #跟随Nacos的负载均衡配置
6.2.2、配置Nacos:

Nacos里面设置实例的权重即可。
在这里插入图片描述

6.3、自定义策略

自定义策略,你要是有负载均衡方案的话其实很简单。我们直接模仿内置的Random策略就可以编写负载均衡算法以及应用上去。

6.3.1、编写负载均衡算法

我这里自定义一个非常简单的加权轮询算法。

package com.zhjn.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.*;
import reactor.core.publisher.Mono;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * Author: 千秋
 * Date: 2025/10/30/15:52
 * Description:
 */
public class WeightedRoundRobin implements ReactorServiceInstanceLoadBalancer {

    private final String serviceId;
    private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
    //当前索引
    private AtomicInteger currentIndex = new AtomicInteger(-1);

    public WeightedRoundRobin(String serviceId, ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider) {
        this.serviceId = serviceId;
        this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;
    }

    @Override
    public Mono<Response<ServiceInstance>> choose(Request request) {
        ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
        return supplier.get().next().map(instances -> {
            return this.processInstanceResponse(supplier,instances);
        });
    }
    private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier,List<ServiceInstance> instances) {
        Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(instances);
        if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
            ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
        }

        return serviceInstanceResponse;

    }

    /**
     * 加权轮询算法
     * @param instances
     * @return
     */
    private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances){
        if (instances.isEmpty()) {
            System.out.println("No servers available for service: " + this.serviceId);
            return new EmptyResponse();
        } else {

            List<ServiceInstance> weightedInstances = new ArrayList<>();
            //遍历所有实例,按权重数值,添加同等数值的同实例到weightedInstances集合中。
            instances.forEach(instance -> {
                double weightFromMetadata = getWeightFromMetadata(instance);
                for(int i = 0; i < weightFromMetadata; i++){
                    weightedInstances.add(instance);
                }
            });
            //当索引超出范围时,重置为初始值
            if(currentIndex.intValue() >= weightedInstances.size() - 1){
                currentIndex = new AtomicInteger(-1);
            }
            //获取下一个实例
            currentIndex.addAndGet(1);

            ServiceInstance instance = weightedInstances.get(currentIndex.intValue());
            return new DefaultResponse(instance);
        }
    }
    
    /**
     * 从Nacos元数据获取权重
     * Nacos权重存储在metadata的"nacos.weight"字段中
     */
    private double getWeightFromMetadata(ServiceInstance instance) {
        Map<String, String> metadata = instance.getMetadata();

        String weightStr = metadata.get("nacos.weight");
        if (weightStr == null) {
            weightStr = metadata.get("weight");
        }
        if (weightStr == null) {
            // 默认权重
            return 1.0;
        }

        try {
            return Double.parseDouble(weightStr);
        } catch (NumberFormatException e) {
            return 1.0;
        }
    }
}

6.3.2、应用负载均衡策略

配置类和前面更新随机策略是一样的。

package com.zhjn.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;

/**
 * Author: 千秋
 * Date: 2025/10/30/14:32
 * Description:
 */
public class CustomLoadBalancerConfiguration {
    /**
     * 更换为加权轮询负载均衡策略
     * @param environment
     * @param loadBalancerClientFactory
     * @return
     */
    @Bean
    ReactorLoadBalancer<ServiceInstance> weightedRoundRobinLoadBalancer(Environment environment,
                                                            LoadBalancerClientFactory loadBalancerClientFactory) {

        String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
        return new WeightedRoundRobin(name,loadBalancerClientFactory
                .getLazyProvider(name, ServiceInstanceListSupplier.class));
    }
}

最后,在启动类上添加**@LoadBalancerClient**注解,和切换内置随机策略是一模一样的。

7、请求/响应压缩

请求/响应压缩,属于网络I/O优化,但是压缩和解压缩也会消耗CPU计算资源,本质上是以计算换传输,通过拦截器机制实现的。适合需要节省带宽、降低网络延迟,加快传输速度、减轻网络设备(路由器、交换机、负载均衡器等等)压力的场景。

7.1、application.yml属性配置:
spring:
  cloud:
    openfeign:
      compression:  #压缩设置
        request:  #请求端
          enabled: false #开启请求压缩
          mime-types: text/xml,application/xml,application/json,application/octet-stream    #压缩的媒体类型
          min-request-size: 2048  # 触发压缩的最小请求体大小,默认 2048 字节(2KB)
        response: #响应端
          enabled: false #开启响应压缩
7.2、服务端解压缩

服务识别到请求头信息中包含Content-Encoding: gzip,那么会对请求体进行解压,这个解压是由底层HTTP客户端来实现的。

7.2.1、application.yml文件属性配置:
server:
  compression:  #压缩设置
    enabled: true

8、Capability(能力)

这玩意是用来增强OpenFeign客户端的,比如说,MicrometerObservationCapability(监控微服务间的 HTTP调用)、断路器容错能力(Resilience4j、Sentinel)、日志记录能力(Logging),就是典型的OpenFeign增强。既可以用知名厂商的,也可以自定义。

9、缓存

OpenFeign 缓存指的是在 OpenFeign 客户端层面,对远程服务调用的响应结果进行存储。当下一次有相同的请求发生时,OpenFeign会直接从本地缓存中返回结果,而不会真正发起一次网络调用。类似于浏览器缓存,但它是应用级别的,专门用于缓存微服务间的 HTTP 调用结果。它是基于SpringCache实现的,要会用OpenFeign缓存,那么必须得先掌握Spring的缓存

9.1、Spring缓存
9.1.1、Spring缓存的配置文件属性:
配置属性 说明 示例值
spring.cache.type 强制指定缓存提供者的类型 redis, caffeine, ehcache, none
spring.cache.redis.time-to-live 缓存条目的存活时间(TTL) 36000(毫秒), 20s
spring.cache.redis.key-prefix 缓存键的自定义前缀 spring-cache
spring.cache.redis.use-key-prefix 是否使用键前缀 true(默认) / false
spring.cache.redis.cache-null-values 是否缓存空值(用于防止缓存穿透) true/ false(默认)
spring.cache.redis.enable-statistics 是否启用缓存统计 true/ false(默认)
spring.cache.caffeine.spec 用于配置 Caffeine 缓存的具体规格(如最大大小、过期时间) maximumSize=500, expireAfterAccess=600s
9.1.2、Spring缓存注解:

@EnableCaching:启用缓存模块。在启动类上添加。

@Cacheable:标记方法的返回值应当被缓存。首次调用后,相同参数的调用将直接返回缓存值,不再执行方法体

适用场景:数据变更不频繁的查询方法。

内部属性

  1. value/ cacheNames:指定缓存名称(必填),可关联多个缓存。它定义了结果存储在哪个逻辑缓存区中。
  2. key:缓存的key,用于生成缓存项唯一标识的 SpEL 表达式。它决定了如何区分不同参数的调用结果,设置了那么值就不能为空,但是可以不设置。可设置为"#p0",意思是取第一个参数的值作为唯一标识。也可以#id这样,去对应参数的值作为缓存唯一标识,但是有一个问题是,需要启用 -parameters,否则报错。不设置也行,Spring其实会根据方法名、参数类型、参数值生成一个,但是这样的话,可能会引入缓存失效或误命中。
  3. unless: 缓存条件,执行方法后决定缓存是否生效的 SpEL 表达式。
  4. condition: 缓存条件,执行方法前决定缓存是否会尝试写入。
  5. sync:同步:默认为 false。若设为 true,在缓存缺失时会对方法调用加锁,防止缓存击穿(仅部分缓存实现如 Caffeine 支持)。

@CachePut:总是执行方法体,并将结果更新到缓存中。

适应场景:数据更新操作后,确保缓存与数据库同步。其属性与 @Cacheable基本相同,但没有 sync属性。

@CacheEvict:移除一个或多个缓存项。

适用场景:数据删除或变更后,使旧缓存失效。

内部属性

  1. allEntries:是否清空整个缓存区:默认为 false,只删除由 key指定的项。若设为 true,会清除 value/cacheNames指定的缓存区内的所有数据。
  2. beforeInvocation:清除时机:默认为 false,表示在方法成功执行后才清除缓存。若设为 true,则在方法执行前清除,即使方法后续执行失败也会清除缓存。

@Caching:用于组合多个缓存操作注解,应对复杂场景。
示例:可同时在一个方法上指定 @Cacheable, @CachePut, @CacheEvict。
@CacheConfig:类级别注解,用于统一配置该类中所有缓存注解的公共属性,如 cacheNames, keyGenerator等,减少重复配置。

9.1.3、手动操作缓存:
操作类别 核心方法 说明与示例
获取 Cache 实例 CacheManager.getCache(String cacheName) 通过缓存名称获取特定的 Cache 对象。这是所有手动操作的起点
存储/更新缓存 Cache.put(Object key, Object value) 将键值对存入缓存。若 key 已存在,则覆盖旧值
获取缓存 Cache.get(Object key) 根据 key 获取缓存项,返回 ValueWrapper对象。需检查是否为 null来判断缓存是否存在
获取缓存 Cache.get(Object key, Class type) 直接返回指定类型的值,更简洁。若缓存不存在或类型不匹配则返回 null
条件性获取 Cache.get(Object key, Callable valueLoader) 尝试获取缓存,若不存在则通过传入的 Callable加载数据并存入缓存,最后返回结果
删除单条缓存 Cache.evict(Object key) 从缓存中移除指定 key 对应的缓存项
清空整个缓存区 Cache.clear() 清空指定缓存名称下的所有缓存项
9.2、OpenFeign中应用
9.2.1、在启动类上,添加@EnableCaching注解,启用Spring Framework的缓存支持。
package com.zhjn;

import com.zhjn.config.CustomLoadBalancerConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
 * Author: 千秋
 * Date: 2025/08/28/14:28
 * Description:
 */
@SpringBootApplication
@EnableDiscoveryClient // 启用SpringCloud的原生注解启动服务注册与发现
@EnableFeignClients    // 启用Feign声明式客户端
//针对某个服务,指定负载均衡策略
//@LoadBalancerClient(name = "check-server", configuration = CustomLoadBalancerConfiguration.class)
//针对所有服务,指定负载均衡策略
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfiguration.class)
//启用缓存
@EnableCaching
public class AuthApplication {
    public static void main(String[] args) {
        SpringApplication.run(AuthApplication.class, args);
        System.out.println("-------------------------------------");
        System.out.println("认证授权服务启动成功!");
        System.out.println("-------------------------------------");
    }
}
9.2.2、applicatiion.yml文件配置属性
spring:
  cache: #缓存配置
    type: caffeine  #缓存类型:caffeine是本地的caffeine缓存,可以把它理解为一个超级加强版的 ConcurrentHashMap。
    caffeine: # caffeine缓存配置,maximumSize:最大存储数量。expireAfterAccess: 缓存失效时间
      spec: maximumSize=500,expireAfterAccess=600s
9.2.3、在OpenFeign 客户端应用缓存
package com.zhjn.check;

import org.springframework.cache.annotation.Cacheable;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;


/**
 * Author: 千秋
 * Date: 2025/09/30/10:02
 * Description:  OpenFeign客户端接口
 */

@FeignClient(name = "check-server", contextId = "TestClient", path = "/api/check/test")
public interface TestClient {

    @GetMapping
    @Cacheable(value = "bytes", key = "#p0", unless = "#result == null")
    byte[] test2(@RequestParam("id") String id);

}

10、全

10.1、代码配置(全局/局部):
package com.zhjn.config;

import feign.Logger;
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


/*
 * Feign配置类(全局)
 */
@Configuration
public class FeignConfig {

    /**
     * 设置feign日志级别
     *  1. None:无日志记录( 默认)。性能最佳,适合生产环境。
     *  2. BASIC:仅记录请求方法和 URL 以及响应状态代码和执行时间。适用于生产环境追踪问题。
     *  3. HEADERS:记录基本信息以及请求和响应标头。
     *  4. FULL:记录请求和响应的标头、正文和元数据。适合开发测试环境定位问题。
     * @return
     */
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
    
    /**
     * 设置feign超时时间
     * @return
     */
    @Bean
    public Request.Options options() {

        //连接时间:单位是毫秒。默认是10s,不同版本或有差异。
        //建议设置1-5秒之间。TCP的三次握手的时间的非常快的,所以设置短一点。
        int connectTimeoutMillis = 5000;
        //读取时间:单位是毫秒。默认是60s,不同版本或有差异。根据业务需求设置,一般是5-30秒之间。
        int readTimeoutMillis = 5000;
        //接口的总时间 = connectTimeoutMillis + readTimeoutMillis
        return new Request.Options(connectTimeoutMillis, readTimeoutMillis);
    }

    /**
     * 设置feign重试次数
     * @return
     */
    @Bean
    public Retryer feignRetryer() {
        //参数1:period: 初始重试间隔时间,单位毫秒
        //第一次失败后,等待100ms再重试。
        //参数2:maxPeriod: 最大重试间隔时间,单位毫秒
        //重试间隔会逐渐增加(指数增长),但不会超过 1000ms。
        // 100、200、400、800...1000 ms
        //参数3:maxAttempts: 最大重试次数
        //总共最多尝试3次,包括第一次调用。也就是重试2次。
        return new Retryer.Default(100, 1000, 3); // 最多重试2次
    }

    /**
     * 自定义feign拦截器
     * @return
     */
    @Bean
    public CustomIntercept customIntercept() {
        return new CustomIntercept();
    }

}


全局配置,在上诉的FeignConfig文件中添加@Configuratiion注解即可。可局部配置还要多一步,需要 在需要局部配置的客户端接口上把配置文件加载进去。如下:

package com.zhjn.check;

import com.zhjn.check.entity.UserVO;
import com.zhjn.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;

/**
 * Author: 千秋
 * Date: 2025/09/30/10:02
 * Description:  
 */

@FeignClient(name = "check-server", contextId = "TestClient", path = "/api/check/test",configuration = FeignConfig.class)
public interface TestClient {

    @PostMapping
    String test(UserVO userVO);

}

10.2、application.yml文件属性配置:
server:
  port: 8889
  compression:
    enabled: true
spring:
  cache: #缓存配置
    type: caffeine  #缓存类型:caffeine是本地的caffeine缓存,可以把它理解为一个超级加强版的 ConcurrentHashMap。
    caffeine: # caffeine缓存配置,maximumSize:最大存储数量。expireAfterAccess: 缓存失效时间
      spec: maximumSize=500,expireAfterAccess=600s
  cloud:
    nacos:
      discovery:
        server-addr: xx.xx.xx.xx
        namespace: xxxxx
        username: nacos
        password: nacos
      config:
        enabled: true
        server-addr: xx.xx.xx.xx
        namespace: xxxxxx
        username: nacos
        password: nacos
        file-extension: yaml
        refresh-enabled: true
    openfeign:
      #Http客户端
      okhttp:
        enabled: true
      #客户端设置
      client:
        config:
          #feignName设置为default是全局的意思。
          #可设服务名,就针对服务进行配置了。
          #也可设置client的contextId,这样局可针对客户端进行配置了。
          TestClient:
            # 日志级别
            # 1、None:无日志记录(默认)。性能最佳,适合生产环境。
            # 2、BASIC:仅记录请求方法、URL、响应状态码和执行时间,适用于生产环境追踪问题。
            # 3、HEADERS:除了BASIC中定义的信息外,还会记录请求和响应的头信息。
            # 4、FULL:除了HEADERS中定义的信息外,还会记录请求和响应的正文及元数据,适合开发测试环境定位问题。
            loggerLevel: full
            # 连接超时:建立TCP连接的超时时间,从开始尝试连接到成功建立连接的时间。
            connect-timeout: 2000
            # 读取超时:从连接建立成功后,到接收到完整响应的时间,包括服务器处理时间和网络传输时间。
            read-timeout: 5000
            #添加Feign的拦截器 [0]这个下表代表拦截器的顺序
            request-interceptors[0]: com.zhjn.config.CustomIntercept
    #loadBalancer的配置
    #loadbalancer:
      #nacos:
        #enabled: true  #跟随Nacos的负载均衡配置
      compression:  #压缩设置
        request:  #请求端
          enabled: false #开启请求压缩
          mime-types: text/xml,application/xml,application/json,application/octet-stream    #压缩的媒体类型
          min-request-size: 2048  # 触发压缩的最小请求体大小,默认 2048 字节(2KB)
        response: #响应端
          enabled: false #开启响应压缩

  config:
    import:
      - optional:nacos:${spring.application.name}-dev
logging:
  level:
    org.springframework.web.client.RestTemplate: DEBUG
    com.zhjn.check: debug     #com.zhjn.check是客户端
    org.springframework.cache: debug

五、常见问题

1、OpenFeign首次调用慢。

首次调用需完成一系列初始化操作(如加载配置、创建代理、服务发现、建立连接池等),导致延迟较高。可以在程序启动的时候,进行初始化,随便发起一个请求调用。说穿了就是把懒加载变成饥饿加载。

2、在项目开发中当OpenFeign遇上统一异常出现问题。

处理的方法有很多。可以在服务提供者那边,重设一下OpenFeign返回的异常处理。也可以在服务消费者这边解决,主要解决思路是对响应进行“二次校验”,将格式上的“成功”转换为业务逻辑上的异常。

例1:自定义一个ErrorDecoder,当 HTTP 状态码非 200 时,它会抛出异常;当状态码为 200 但响应体指示业务错误时,抛出异常。

/**
 * 自定义Feign错误解码器
 */
@Component
public class FeignCustomErrorDecoder implements ErrorDecoder {

    @Override
    public Exception decode(String methodKey, Response response) {
        // 1. 如果HTTP状态码不是200,使用默认解码器处理(会抛出FeignException)
        if (response.status() != 200) {
            return new Default().decode(methodKey, response);
        }
        // 2. 状态码是200,但需要检查响应体是否为错误结构
        try {
            // 读取响应流
            String body = Util.toString(response.body().asReader(StandardCharsets.UTF_8));
            // 解析JSON,判断是否为错误(例如code字段不是200)
            JsonNode jsonNode = new ObjectMapper().readTree(body);
            if (jsonNode.has("code") && jsonNode.get("code").asInt() != 200) {
                String errorMsg = jsonNode.has("msg") ? jsonNode.get("msg").asText() : "处理统一处理异常,状态码问题";
                // 抛出自定义业务异常,这样消费者端的@ControllerAdvice可以捕获
                throw new BusinessException(jsonNode.get("code").asInt(), errorMsg);
            }
            // 如果不是错误结构,返回null,Feign会继续正常流程
            return null;
        } catch (IOException e) {
            throw new DecodeException(response.status(), "Error decoding response", response.request(), e);
        }
    }
}

例2:使用 AOP 切面统一处理,拦截所有Feign客户端方法的返回值,进行检查。

@Aspect
@Component
public class FeignResponseAspect {

    @Around("@within(org.springframework.cloud.openfeign.FeignClient)")
    public Object checkFeignResponse(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = joinPoint.proceed();
        // 如果返回结果是统一的响应结构
        if (result instanceof CommonResult) {
            CommonResult commonResult = (CommonResult) result;
            // 判断业务码是否成功
            if (!commonResult.isSuccess()) {
                // 抛出异常,由全局异常处理器捕获
                throw new BusinessException(commonResult.getCode(), commonResult.getMsg());
            }
        }
        return result;
    }
}
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐