Spring Cloud Alibaba之負(fù)載均衡組件 - Ribbon詳解(三)

Ribbon是Netflix公司開源的一個(gè)負(fù)載均衡的項(xiàng)目(https://github.com/Netflix/ribbon),它是一個(gè)基于HTTP、TCP的客戶端負(fù)載均衡器。

服務(wù)端負(fù)載均衡

負(fù)載均衡是微服務(wù)架構(gòu)中必須使用的技術(shù),通過負(fù)載均衡來實(shí)現(xiàn)系統(tǒng)的高可用、集群擴(kuò)容等功能。負(fù)載均衡可通過硬件設(shè)備及軟件來實(shí)現(xiàn),硬件比如:F5、Array等,軟件比如:LVS、Nginx等。


用戶請求先到達(dá)負(fù)載均衡器(也相當(dāng)于一個(gè)服務(wù)),負(fù)載均衡器根據(jù)負(fù)載均衡算法將請求轉(zhuǎn)發(fā)到微服務(wù)。負(fù)載均衡算法有:輪訓(xùn)、隨機(jī)、加權(quán)輪訓(xùn)、加權(quán)隨機(jī)、地址哈希等方法,負(fù)載均衡器維護(hù)一份服務(wù)列表,根據(jù)負(fù)載均衡算法將請求轉(zhuǎn)發(fā)到相應(yīng)的微服務(wù)上,所以負(fù)載均衡可以為微服務(wù)集群分擔(dān)請求,降低系統(tǒng)的壓力

客戶端負(fù)載均衡

上圖是服務(wù)端負(fù)載均衡,客戶端負(fù)載均衡與服務(wù)端負(fù)載均衡的區(qū)別在于客戶端要維護(hù)一份服務(wù)列表,Ribbon從Eureka Server獲取服務(wù)列表,Ribbon根據(jù)負(fù)載均衡算法直接請求到具體的微服務(wù),中間省去了負(fù)載均衡服務(wù)。

Ribbon負(fù)載均衡的流程圖:


  • 在消費(fèi)微服務(wù)中使用Ribbon實(shí)現(xiàn)負(fù)載均衡,Ribbon先從Eureka Server 或 Nacos Server中獲取服務(wù)列表。

  • Ribbon根據(jù)負(fù)載均衡的算法去調(diào)用微服務(wù)。

Ribbon測試

Spring Cloud引入Ribbon配合 restTemplate 實(shí)現(xiàn)客戶端負(fù)載均衡。Java中遠(yuǎn)程調(diào)用的技術(shù)有很多,如:webservice、socket、rmi、Apache HttpClient、OkHttp等。

  1. 在客戶端添加Ribbon依賴

注意:由于我們之前整合Nacos時(shí)引入了spring-cloud-starter-alibaba-nacos-discovery這個(gè)依賴包,而這個(gè)包默認(rèn)已經(jīng)幫我們繼承了Ribbon,所有這里可以不用單獨(dú)引入Ribbon依賴包。

  1. 配置Ribbon參數(shù)
ribbon:
  MaxAutoRetries: 2  #最大重試次數(shù),當(dāng)Eureka中可以找到服務(wù),但是服務(wù)連不上時(shí)將會(huì)重試
  MaxAutoRetriesNextServer: 3  #切換實(shí)例的重試次數(shù)
  OkToRetryOnAllOperations: false  #對所有操作請求都進(jìn)行重試,如果是get則可以,如果是post,put等操作沒有實(shí)現(xiàn)冪等的情況下是很危險(xiǎn)的,所以設(shè)置為false
  ConnectTimeout: 5000 #請求連接的超時(shí)時(shí)間
  ReadTimeout: 6000  #請求處理的超時(shí)時(shí)間
  1. 負(fù)載均衡測試

啟動(dòng)兩個(gè)服務(wù),端口需要不一致

定義RestTemplate,使用@LoadBalanced注解

    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

測試代碼

@Slf4j
@RestController
public class TestController {
    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/test")
    public String test() {
        String result = restTemplate.getForObject("http://alibaba-nacos-discovery-server/hello?name=wolf", String.class);
        return "Return : " + result;
    }
}

可以看到,在定義RestTemplate時(shí)候,增加了@LoadBalanced注解,而在真正調(diào)用服務(wù)接口的時(shí)候,原來host部分是通過手工拼接ip和端口形式。而這里直接采用服務(wù)名來寫請求路徑即可。在真正調(diào)用的時(shí)候,Spring Cloud會(huì)將請求攔截下來,然后通過Ribbon從Nacos Server獲取服務(wù)列表,并通過負(fù)載均衡器選出節(jié)點(diǎn),并替換服務(wù)名部分為具體的ip和端口,交給RestTemplate去請求,從而實(shí)現(xiàn)基于服務(wù)名的負(fù)載均衡調(diào)用。

Ribbon饑餓加載

默認(rèn)情況下Ribbon是懶加載的。當(dāng)服務(wù)起動(dòng)好之后,第一次請求是非常慢的,第二次之后就快很多。

解決方式:開啟饑餓加載

ribbon:
 eager-load:
  enabled: true #開啟饑餓加載
  clients: server-1,server-2,server-3 #為哪些服務(wù)的名稱開啟饑餓加載,多個(gè)用逗號分隔

Ribbon組件

接口 作用 默認(rèn)值
IclientConfig 讀取配置 DefaultClientConfigImpl
IRule 負(fù)載均衡規(guī)則,選擇實(shí)例 ZoneAvoidanceRule
IPing 篩選掉ping不通的實(shí)例 DummyPing(該類什么不干,認(rèn)為每個(gè)實(shí)例都可用,都能ping通)
ServerList<Server> 交給Ribbon的實(shí)例列表 Ribbon:ConfigurationBasedServerList
Spring Cloud Alibaba:NacosServerList
ServerListFilter<Server> 過濾掉不符合條件的實(shí)例 ZonePreferenceServerListFilter
ILoadBalancer Ribbon的入口 ZoneAwareLoadBalancer
ServerListUpdater 更新交給Ribbon的List的策略 PollingServerListUpdater

這里的每一項(xiàng)都可以自定義:
IclientConfig Ribbon支持非常靈活的配置就是有該組件提供的
IRule 為Ribbon提供規(guī)則,從而選擇實(shí)例、該組件是最核心組件

舉例:

  • 代碼方式:
@Configuration
public class RibbonRuleConfig {
    @Bean
    public IRule ribbonRulr() {
        return new RandomRule();
    }
    @Bean
    public IPing iPing(){
        return new PingUrl();
    }
}
  • 配置屬性方式:
<clientName>:
 ribbon:
  NFLoadBalancerClassName: #ILoadBalancer該接口實(shí)現(xiàn)類
  NFLoadBalancerRuleClassName: #IRule該接口實(shí)現(xiàn)類
  NFLoadBalancerPingClassName: #Iping該接口實(shí)現(xiàn)類
  NIWSServerListClassName: #ServerList該接口實(shí)現(xiàn)類
  NIWSServerListFilterClassName: #ServiceListFilter該接口實(shí)現(xiàn)類

在這些屬性中定義的類優(yōu)先于使用@RibbonClient(configuration=RibbonConfig.class)Spring 定義的bean 以及由Spring Cloud Netflix提供的默認(rèn)值。描述:配置文件中定義ribbon優(yōu)先代碼定義

Ribbon負(fù)載均衡的八種算法,其中ResponseTimeWeightedRule已廢除

規(guī)則名稱 特點(diǎn)
AvailabilityFilteringRule 過濾掉一直連接失敗的被標(biāo)記為circuit tripped(電路跳閘)的后端Service,并過濾掉那些高并發(fā)的后端Server或者使用一個(gè)AvailabilityPredicate來包含過濾Server的邏輯,其實(shí)就是檢查status的記錄的各個(gè)Server的運(yùn)行狀態(tài)
BestAvailableRule 選擇一個(gè)最小的并發(fā)請求的Server,逐個(gè)考察Server,如果Server被tripped了,則跳過
RandomRule 隨機(jī)選擇一個(gè)Server
ResponseTimeWeightedRule 已廢棄,作用同WeightedResponseTimeRule
RetryRule 對選定的負(fù)責(zé)均衡策略機(jī)上充值機(jī)制,在一個(gè)配置時(shí)間段內(nèi)當(dāng)選擇Server不成功,則一直嘗試使用subRule的方式選擇一個(gè)可用的Server
RoundRobinRule 輪詢選擇,輪詢index,選擇index對應(yīng)位置Server
WeightedResponseTimeRule 根據(jù)相應(yīng)時(shí)間加權(quán),相應(yīng)時(shí)間越長,權(quán)重越小,被選中的可能性越低
ZoneAvoidanceRule (默認(rèn)是這個(gè))負(fù)責(zé)判斷Server所Zone的性能和Server的可用性選擇Server,在沒有Zone的環(huán)境下,類似于輪詢(RoundRobinRule)

實(shí)現(xiàn)負(fù)載均衡<細(xì)粒度>配置-隨機(jī)

  • 方式一:代碼方式
    首先定義RestTemplate,并且添加注解@LoadBalanced,這樣RestTemplate就實(shí)現(xiàn)了負(fù)載均衡
@LoadBalanced
@Bean
public RestTemplate restTemplate() {
//template.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));//解決中文亂碼
return new RestTemplate();
}

在SpringBootApplication主類下添加配置類。該類主要作用于為哪個(gè)服務(wù)做負(fù)載均衡。默認(rèn)的是輪訓(xùn)

@Configuration
@RibbonClient(name = "${服務(wù)名稱}", configuration = GoodsRibbonRuleConfig.class)//configuration: 指向負(fù)載均衡規(guī)則的配置類
public class GoodsRibbonConfig {
}

添加Ribbon的配置類,注意該類必須配置在@SpringBootApplication主類以外的包下。不然的話所有的服務(wù)都會(huì)按照這個(gè)規(guī)則來實(shí)現(xiàn)。會(huì)被所有的RibbonClient共享。主要是主類的主上下文和Ribbon的子上下文起沖突了。父子上下文不能重疊

@Configuration
public class GoodsRibbonRuleConfig {
    @Bean
    public IRule ribbonRulr() {
        return new RandomRule();
    }
}
  • 方式二:配置屬性方式
server-1: # 服務(wù)名稱 Service-ID
  ribbon:
    # 屬性配置方式【推薦】
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #  配置文件配置負(fù)載均衡算法-我這里使用的是自定義的Ribbon的負(fù)載均衡算法,默認(rèn)

優(yōu)先級:配置(不會(huì)影響其他服務(wù))>(大于) 硬編碼(類得寫在SpringBoot啟動(dòng)類包外,不然會(huì)影響其他服務(wù))

總結(jié):

配置方式 優(yōu)點(diǎn) 缺點(diǎn)
代碼配置 基于代碼,更加靈活 有坑(父子上下文)
線上修改得重新打包,發(fā)布
屬性配置 易上手 配置更加直觀
線上修改無需重新打包,發(fā)布
優(yōu)先級更高
極端場景下沒有配置配置方式靈活

實(shí)現(xiàn)負(fù)載均衡<全局>配置-隨機(jī)

  • 方式一:Ribbon的配置類定義在主類下
    讓ComponentScan上下文重疊(強(qiáng)烈不建議使用)
  • 方式二:
@Configuration
@RibbonClients(defaultConfiguration = GoodsRibbonRuleConfig.class)//Ribbon負(fù)載均衡全局粒度配置(所有服務(wù)都按照這個(gè)配置)
public class RibbonConfig {
}

擴(kuò)展Ribbon-支持Nacos權(quán)重

默認(rèn)情況下Ribbon是不支持Nacos的權(quán)重負(fù)載均衡選擇的,這里我們自己擴(kuò)展一個(gè)Rule,然Ribbon支持Nacos的權(quán)重規(guī)則。

  1. 創(chuàng)建NacosWeightedRule類
package com.thtf.contentcenter.configuration;

import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import com.alibaba.nacos.api.naming.pojo.Instance;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.Server;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.alibaba.nacos.NacosDiscoveryProperties;
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer;


@Slf4j
public class NacosWeightedRule extends AbstractLoadBalancerRule {
    
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;
    
    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
        //讀取配置文件,并初始化
    }

    @Override
    public Server choose(Object o) {
        try {
            BaseLoadBalancer loadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
            //想要請求的微服務(wù)名稱
            String name = loadBalancer.getName();
            //實(shí)現(xiàn)負(fù)載均衡算法
            //拿到服務(wù)發(fā)現(xiàn)相關(guān)API
            NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
            //nacos client自動(dòng)通過基于權(quán)重的負(fù)載均衡算法,給我們選擇一個(gè)實(shí)例。
            Instance instance = namingService.selectOneHealthyInstance(name);
            
            log.info("選擇的實(shí)例是:port = {}, instance = {}", instance.getPort(), instance);
            return new NacosServer(instance);
        } catch (NacosException e) {
            log.error("選擇實(shí)例異常:{}", e.getMessage(), e);
            return null;
        }
    }
}
  1. 創(chuàng)建RibbonConfiguration類
package com.thtf.contentcenter.ribbonconfiguration;

import com.istimeless.contentcenter.configuration.NacosWeightedRule;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfiguration {
    
    @Bean
    public IRule ribbonRule() {
        return new NacosWeightedRule();
    }
}

特別注意:RibbonConfiguration要建在啟動(dòng)類掃描不到的地方,如圖所示:

  1. 創(chuàng)建UserCenterRibbonConfiguration類,實(shí)現(xiàn)全局配置
package com.thtf.contentcenter.configuration;

import com.istimeless.ribbonconfiguration.RibbonConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@RibbonClients(defaultConfiguration = RibbonConfiguration.class)
public class UserCenterRibbonConfiguration {
}

擴(kuò)展Ribbon-同集群優(yōu)先

在Nacos上,支持集群配置。集群是指對指定微服務(wù)的一種虛擬分類。集群還是比較有用的,例如:

  • 為了容災(zāi),把指定微服務(wù)同時(shí)部署在兩個(gè)機(jī)房(例如同城多活,其中1個(gè)機(jī)房崩潰了,另一個(gè)機(jī)房還能頂上,異地多活防止自然災(zāi)害)
  • 調(diào)用時(shí),可優(yōu)先調(diào)用同機(jī)房的實(shí)例,如果同機(jī)房沒有實(shí)例,再跨機(jī)房調(diào)用。

雖然Spring Cloud Alibaba支持集群配置,例如:

spring: 
  cloud:    
    nacos:  
      discovery:    
        # 北京機(jī)房集群    
        cluster-name: BJ

但在調(diào)用時(shí),服務(wù)消費(fèi)者并不會(huì)優(yōu)先調(diào)用同集群的實(shí)例。
本節(jié)來探討如何擴(kuò)展Ribbon,從而實(shí)現(xiàn)同集群優(yōu)先調(diào)用的效果,并且還能支持Nacos權(quán)重配置。關(guān)于權(quán)重配置,前面已經(jīng)實(shí)現(xiàn)了,在前面的基礎(chǔ)上實(shí)現(xiàn)同集群優(yōu)先策略。

/** 
 * 支持優(yōu)先調(diào)用同集群實(shí)例的ribbon負(fù)載均衡規(guī)則.    
 *  
 * @author itmuch.com   
 */ 
@Slf4j  
public class NacosRule extends AbstractLoadBalancerRule {   
    @Autowired  
    private NacosDiscoveryProperties nacosDiscoveryProperties;  
    
    @Override   
    public Server choose(Object key) {  
        try {   
            String clusterName = this.nacosDiscoveryProperties.getClusterName();    
            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer(); 
            String name = loadBalancer.getName();   
    
            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();    
        // 1. 找到指定服務(wù)的所有實(shí)例 A
            List<Instance> instances = namingService.selectInstances(name, true);   
            if (CollectionUtils.isEmpty(instances)) {   
                return null;    
            }   
    
            List<Instance> instancesToChoose = instances;   
            if (StringUtils.isNotBlank(clusterName)) {  
                // 2. 過濾出相同集群下的所有實(shí)例 B
                List<Instance> sameClusterInstances = instances.stream()    
                        .filter(instance -> Objects.equals(clusterName, instance.getClusterName())) 
                        .collect(Collectors.toList());  
                // 3. 如果B為空,就用A
                if (!CollectionUtils.isEmpty(sameClusterInstances)) {   
                    instancesToChoose = sameClusterInstances;   
                } else {    
                    log.warn("發(fā)生跨集群的調(diào)用,name = {}, clusterName = {}, instance = {}", name, clusterName, instances);  
                }   
            }   
        // 4. 基于權(quán)重的負(fù)載均衡算法,返回一個(gè)實(shí)例
            Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToChoose);   
       
            return new NacosServer(instance);   
        } catch (Exception e) { 
            log.warn("NacosRule發(fā)生異常", e);   
            return null;    
        }   
    }   
    
    @Override   
    public void initWithNiwsConfig(IClientConfig iClientConfig) {   
    }   
}

負(fù)載均衡算法:

// Balancer來自于com.alibaba.nacos.client.naming.core.Balancer,也就是Nacos Client自帶的基于權(quán)重的負(fù)載均衡算法。  
public class ExtendBalancer extends Balancer {  
    /** 
     * 根據(jù)權(quán)重,隨機(jī)選擇實(shí)例  
     *  
     * @param instances 實(shí)例列表    
     * @return 選擇的實(shí)例    
     */ 
    public static Instance getHostByRandomWeight2(List<Instance> instances) {   
        return getHostByRandomWeight(instances);    
    }   
}

配置:

microservice-provider-user: 
  ribbon:   
    NFLoadBalancerRuleClassName: com.itmuch.cloud.study.ribbon.NacosClusterAwareWeightedRule

這樣,服務(wù)在調(diào)用microservice-provider-user 這個(gè)服務(wù)時(shí),就會(huì)優(yōu)先選擇相同集群下的實(shí)例。

擴(kuò)展Ribbon-基于元數(shù)據(jù)的版本控制

至此,已經(jīng)實(shí)現(xiàn)了

  • 優(yōu)先調(diào)用同集群下的實(shí)例
  • 實(shí)現(xiàn)基于權(quán)重配置的負(fù)載均衡
    但實(shí)際項(xiàng)目,我們可能還會(huì)有這樣的需求:
    一個(gè)微服務(wù)在線上可能多版本共存,例如:
  • 服務(wù)提供者有兩個(gè)版本:v1、v2
  • 服務(wù)消費(fèi)者也有兩個(gè)版本:v1、v2
    v1/v2是不兼容的。服務(wù)消費(fèi)者v1只能調(diào)用服務(wù)提供者v1;消費(fèi)者v2只能調(diào)用提供者v2。如何實(shí)現(xiàn)呢?

下面圍繞該場景,實(shí)現(xiàn)微服務(wù)之間的版本控制。

元數(shù)據(jù)
元數(shù)據(jù)就是一堆的描述信息,以map存儲(chǔ)。舉個(gè)例子:

spring:
  cloud:
    nacos:
        metadata: 
          # 自己這個(gè)實(shí)例的版本
          version: v1
          # 允許調(diào)用的提供者版本
          target-version: v1

需求分析

我們需要實(shí)現(xiàn)的有兩點(diǎn):

  • 優(yōu)先選擇同集群下,符合metadata的實(shí)例
  • 如果同集群加沒有符合metadata的實(shí)例,就選擇所有集群下,符合metadata的實(shí)例

代碼實(shí)現(xiàn)

@Slf4j
public class NacosFinalRule extends AbstractLoadBalancerRule {
    @Autowired
    private NacosDiscoveryProperties nacosDiscoveryProperties;

    @Override
    public Server choose(Object key) {
        // 負(fù)載均衡規(guī)則:優(yōu)先選擇同集群下,符合metadata的實(shí)例
        // 如果沒有,就選擇所有集群下,符合metadata的實(shí)例

        // 1. 查詢所有實(shí)例 A
        // 2. 篩選元數(shù)據(jù)匹配的實(shí)例 B
        // 3. 篩選出同cluster下元數(shù)據(jù)匹配的實(shí)例 C
        // 4. 如果C為空,就用B
        // 5. 隨機(jī)選擇實(shí)例
        try {
            String clusterName = this.nacosDiscoveryProperties.getClusterName();
            String targetVersion = this.nacosDiscoveryProperties.getMetadata().get("target-version");

            DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();
            String name = loadBalancer.getName();

            NamingService namingService = this.nacosDiscoveryProperties.namingServiceInstance();

            // 所有實(shí)例
            List<Instance> instances = namingService.selectInstances(name, true);

            List<Instance> metadataMatchInstances = instances;
            // 如果配置了版本映射,那么只調(diào)用元數(shù)據(jù)匹配的實(shí)例
            if (StringUtils.isNotBlank(targetVersion)) {
                metadataMatchInstances = instances.stream()
                        .filter(instance -> Objects.equals(targetVersion, instance.getMetadata().get("version")))
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(metadataMatchInstances)) {
                    log.warn("未找到元數(shù)據(jù)匹配的目標(biāo)實(shí)例!請檢查配置。targetVersion = {}, instance = {}", targetVersion, instances);
                    return null;
                }
            }

            List<Instance> clusterMetadataMatchInstances = metadataMatchInstances;
            // 如果配置了集群名稱,需篩選同集群下元數(shù)據(jù)匹配的實(shí)例
            if (StringUtils.isNotBlank(clusterName)) {
                clusterMetadataMatchInstances = metadataMatchInstances.stream()
                        .filter(instance -> Objects.equals(clusterName, instance.getClusterName()))
                        .collect(Collectors.toList());
                if (CollectionUtils.isEmpty(clusterMetadataMatchInstances)) {
                    clusterMetadataMatchInstances = metadataMatchInstances;
                    log.warn("發(fā)生跨集群調(diào)用。clusterName = {}, targetVersion = {}, clusterMetadataMatchInstances = {}", clusterName, targetVersion, clusterMetadataMatchInstances);
                }
            }

            Instance instance = ExtendBalancer.getHostByRandomWeight2(clusterMetadataMatchInstances);
            return new NacosServer(instance);
        } catch (Exception e) {
            log.warn("發(fā)生異常", e);
            return null;
        }
    }

    @Override
    public void initWithNiwsConfig(IClientConfig iClientConfig) {
    }
}

** 負(fù)載均衡算法:**

public class ExtendBalancer extends Balancer {
    /**
     * 根據(jù)權(quán)重,隨機(jī)選擇實(shí)例
     *
     * @param instances 實(shí)例列表
     * @return 選擇的實(shí)例
     */
    public static Instance getHostByRandomWeight2(List<Instance> instances) {
        return getHostByRandomWeight(instances);
    }
}

思考

截止到這里,我們已經(jīng)對Ribbon的基本使用已經(jīng)如何自定義Ribbon負(fù)載均衡規(guī)則做了詳細(xì)說明,但細(xì)心的人會(huì)發(fā)現(xiàn),我使用上面 RestTemplate 地址拼接方式調(diào)用服務(wù)接口會(huì)存在以下幾點(diǎn)不足:

  • 代碼可讀性差
  • 復(fù)雜的url接口地址難以維護(hù)
  • 編碼體驗(yàn)不統(tǒng)一

帶著這些不足,我們引入下一章要講解的另一種服務(wù)調(diào)用方式:Feign

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容