今日教學:RestTemplate 結合 Ribbon 使用

無敵天驕發表於2021-04-08
Ribbon 是可以單獨使用的,但是在 Spring Cloud 中使用 Ribbon 會更簡單。因為 Spring Cloud 在 Ribbon 基礎上進行了一層封裝,將很多配置都整合好了。本文將在 Spring Cloud 中使用 Ribbon !

一、使用 RestTemplate 與整合 Ribbon

Spring提供了一種簡單便捷的模板類來進行API的呼叫,那就是 RestTemplate

1. 使用 RestTemplate

首先我們看看GET請求的使用方式:在  fsh-house 服務的 HouseController 中增加兩個介面,一個透過 @RequestParam來傳遞引數,返回一個物件資訊;另一個透過 @PathVarable來傳遞引數,返回一個字串。儘量透過兩個介面組裝不同的形式,具體如下面程式碼所示。

@GetMapping("/data")public HouseInfo getData( @RequestParam("name") String name) {
    return new HouseInfo(1L, "上海","虹口","東體小區");
}
@GetMapping("/data/{name}")
public String getData2(@PathVariable( "name") String name) {
    return name;
}

在  fsh-substitution 服務中用 RestTemplate來呼叫我們剛剛定義的兩個介面,具體如下面程式碼所示。

@GetMapping("/data")
public HouseInfo getData(@RequestParam("name") String name) {
    return restTemplate.getForobject(
        ");
}
    
@GetMapping("/data/{name}")
public String getData2(@PathVariable("name") String name) {
   return restTemplate.getForobject(
      "{name}",String.class,name);
}

獲取資料結果透過 RestTemplate的  getForObject 方法(具體如下面程式碼所示)來實現,此方法有三個過載的實現:

  • url:請求的 API 地址,有兩種方式,其中一種是字串,另一種是 URL 形式。
  • responseType :返回值的型別。
  • uriVariables :  PathVariable 引數,有兩種方式, 其中一種是可變引數,另一種是 Map 形式。
public <T> T getForobject(String url, Class<T> responseType,
    object... uriVariables);
public <T> T getForobject (String url, Class<T> responseType ,
    Map<String, ?> uriVariables) ;
public <T> T getForobject(URI url, Class<T> responseType) ;

除了  getForObject ,我們還可以使用  getForEntity 來獲取資料,程式碼如下面程式碼所示。

@GetMapping(" /data")public HouseInfo getData(@RequestParam("name") String name) {
    ResponseEntity<HouseInfo> responseEntity = restTemplate.getForEntity(
        "http:/ /localhost: 8081 /house/ data?name= "+name, HouseInfo.class) ;
    if (responseEntity.getStatusCodeValue() == 200) {
        return responseEntity.getBody();
    }
    return nu1l ;
}

getForEntity 中可以獲取返回的狀態碼、請求頭等資訊,透過  getBody 獲取響應的內容。其餘的和  getForObject 一樣,也是有3個過載的實現。

接下來看看怎麼使用POST方式呼叫介面。在 HouseController中增加一個save方法用來接收 HouseInfo資料,如下面程式碼所示。

@PostMapping("/save")public Long addData(@RequestBody HouseInfo houseInfo) {
    System.out.println(houseInfo. getName());
    return 1001L;
}

接著寫呼叫程式碼,用 postForObject來呼叫,如下面程式碼所示。

@GetMapping("/save")public Long add(){
    HouseInfo houseInfo = new HouseInfo();
    houseInfo.setCity("上海");
    houseInfo.setRegion("虹口");
    houseInfo.setName( "XXX");
    Long id = restTemplate.postFor0bject(
        "http: //1ocalhost:8081/ house/save",houseInfo,Long.class);
    return id;
}

postForObject 同樣有3個過載的實現。除了  postForObject 還可以使用  postForEntity 方法,用法都一樣,如下面程式碼所示。

public <T> T postForobject(String url, object request,
    Class<T> responseType, object... uriVariables);
    
public <T> T postForobject(String url, object request,
    Class<T> responseType, Map<String, ?> urivariables);
    
public <T> T postForobject(URI url, object request, Class<T> responseType);

除了get和post對應的方法之外, RestTemplate 還提供了 putdelete 等操作方法,還有一個比較實用的就是 exchange方法。 exchange 可以執行 getpost、  put、  delete 這4種請求方式。

2. 整合 Ribbon

在 Spring Cloud 專案中整合 Ribbon 只需要在  pom.xml 中加入下面的依賴即可,其實也可以不用配置,因為 Eureka 中已經引用了  Ribbon , 如下面程式碼所示。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-ribbon</artifactId>
</dependency>

這個配置我們加在 fangjia-fsh-substitution-service中。

二、RestTemplate 負載均衡示例

對之前的程式碼進行一些小改造,輸出一些內容,證明我們整合的 Ribbon 是有效的。

改造 hello 介面,在介面中輸出當前服務的埠,用來區分呼叫的服務,如下面程式碼所示。

@RestController@RequestMapping("/house" )public class HouseController {
    @Value("${server .port}")
    private String serverPort; 
    
    @GetMapping("/hel1o")
    public String hel1o(){
        return "Hello"+serverPort;
    }
}

上述程式碼分別以8081和8083埠啟動兩個服務,後面要用到。

接著改造 callHello 介面的程式碼,將呼叫結果輸出到控制檯,如下面程式碼所示。

@RestController@RequestMapping("/substitution")public class Substitut ionController{
    @Autowired
    private RestTemplate restTemplate;
    @GetMapping ("/cal1Hel1o")
    public String cal1Hello(){
        String result = restTemplate. getFor0bject(
            ");
        System.out.print1n("呼叫結果: " + result);
        return result ;
    }
}

測試步驟如下:

  1. 重啟服務。
  2. 訪問   介面。
  3. 檢視控制檯輸出,此時就知道負載有沒有起作用了。


今日教學:RestTemplate 結合 Ribbon 使用


三、@LoadBalanced 註解原理

相信大家一定有一個疑問:為什麼在  RestTemplate 上加了一個  @LoadBalanced 之後, RestTemplate 就能夠跟 Eureka 結合了,可以使用服務名稱去呼叫介面,還可以負載均衡?

這功勞應歸於Spring Cloud給我們做了大量的底層工作,因為它將這些都封裝好了,我們用起來才會那麼簡單。框架就是為了簡化程式碼,提高效率而產生的。

主要的邏輯就是給  RestTemplate 增加攔截器,在請求之前對請求的地址進行替換,或者根據具體的負載策略選擇服務地址,然後再去呼叫,這就是  @LoadBalanced 的原理。

下面我們來實現一個簡單的攔截器,看看在呼叫介面之前會不會進入這個攔截器。我們不做任何操作,就輸出一句話,證明能進來就行了。如下面程式碼所示。

public class MyLoadBalancerInterceptor implements ClientHttpRequestInterceptor{
    @Override
    public ClientHttpResponse intercept (final HttpRequest request ,
        final byte[] body, final ClientHttpRequestExecution
            execution) throws IOException{
        final URI originalUri = request.getURI(); 
        String serviceName = originalUri.getHost();
        System.out.println("進入自定義的請求攔截器中"+serviceName);
        return execution.execute(request, body);
    }
}

攔截器好了之後,我們再定義一個註解,複製  @LoadBalanced 的程式碼,改個名稱就可以了,如下面程式碼所示。


@Target({ ElementType.FIELD,ElementType.PARAMETER,ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface MyLoadBalanced {
}

然後定義一個配置類,來給 RestTemplate 注人攔截器,如下面程式碼所示。

@Configurationpublic class MyLoadBalancerAutoConfiguration{
    @MyLoadBalanced
    @Autowired(required = false)
    private List<RestTemplate> restTemplates = Collections.emptyList();
    
    @Bean
    public MyLoadBalancerInterceptor myLoadBalancerInterceptor() {
        return new MyLoadBalancerInterceptor();
    }
    
    @Bean
    public SmartInitializingSingleton myLoadBalancedRestTemplateInitializer() {
        return new SmartInitializingSingleton() {
            @Override
            public void afterSingletonsInstantiated() {
                for (RestTemplate restTemplate :
                    MyLoadBalancerAutoConfiguration.this.restTemplates){
                    List<ClientHttpRequestInterceptor> list = new
                        ArrayList<>(restTemplate.getInterceptors());
                    list.add(myLoadBalancerInterceptor());
                    restTemplate.setInterceptors(list);
                }
            }
        };
    }
}

維護一個  @MyLoadBalanced 的 RestTemplate 列表,在  SmartlnitializingSingleton 中對 RestTemplate 進行攔截器設定。

然後改造我們之前的 RestTemplate 配置,將  @LoadBalanced 改成我們自定義的  @MyLoadBalanced,如下面程式碼所示。

@Bean//@LoadBalanced@MyLoadBalancedpublic RestTemplate getRestTemplate(){
    return new RestTemplate() ;
}

重啟服務,訪問   就可以看到控制檯的輸出了,這證明在介面呼叫的時候會進人該攔截器,輸出如下:


今日教學:RestTemplate 結合 Ribbon 使用


透過這個小案例我們就能夠清楚地知道  @LoadBalanced 的工作原理。接下來我們來看看原始碼中是怎樣的一個邏輯。

首先看配置類,如何為  RestTemplate 設定攔截器,程式碼在  spring- cloud commonsjar 中的  orgspringframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration 類裡面透過檢視  LoadBalancerAutoConfiguration 的原始碼,可以看到這裡也是維護了一個  @LoadBalanced 的 RestTemplate 列表,如下面程式碼所示。

@LoadBalanced@Autowired(required = false)private List<RestTemplate> restTemplates = Collections.emptyList();@Beanpublic SmartInitializingSingleton loadBalancedRestTemplateInitializer(
        final List<RestTemplateCustomizer> customizers) {
    return new SmartInitializingSingleton() {
        @Override
        public void afterSingletonsInstantiated() {
            for(RestTemplate restTemplate :
                LoadBalancerAutoConfiguration.this.restTemplates) {
                for (RestTemplateCustomizer customizer:customizers) {
                    customizer.customize(restTemplate);
                
                }
            }
        }
    };
}

下面看看攔截器的配置。可以知道,攔截器用的是 LoadBalancerInterceptorRestTemplate Customizer用來新增攔截器,如下面程式碼所示。

@Configuration @conditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")static class LoadBalancerInterceptorConfig {
    @Bean
    public LoadBalancerInterceptor ribbonInterceptor (
            LoadBalancerClient loadBalancerClient , 
            LoadBalancerRequestFactory requestFactory) 
        return new LoadBalancer Interceptor(loadBalancerClient,requestFactory);
    }
    
    @Bean
    @ConditionalOnMissingBean
    public RestTemplateCustomizer restTemplateCustomizer(
            final LoadBalancerInterceptor loadBalancerInterceptor) {
        return new RestTemplateCustomizer() {
            @Override
            public void customize(RestTemplate restTemplate) {
                List<ClientHttpRequestInterceptor> list = new ArrayList<>(
                    restTemplate.getInterceptors());
                list.add(loadBalancer Interceptor);
                restTemplate. setInterceptors(list);
            }
        };
    }
}

攔截器的程式碼在  org.springframework.cloud.client.loadbalancerLoadBalancerInterceptor 中,如下面程式碼所示。

public class LoadBalancerInterceptor imp1ements
        ClientHttpRequestInterceptor {
    private LoadBalancerClient loadBalancer;
    private LoadBalancerRequestFactory requestFactory;
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer ,
            LoadBalancerRequestFactory requestFactory){
        this. loadBalancer = loadBalancer;
        this. requestFactory = requestFactory;
    }
    public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {
        this(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));
    }
    @Override
    public ClientHttpResponse intercept(final HttpRequest request,
            final byte[] body, final ClientHttpRequestExecution
                execution ) throws IOException {
        final URI originaluri = request. getURI();
        String serviceName = originalUri.getHost();
        Assert.state(serviceName != nu11, "Request URI does not contain a valid hostname:" + originalUri) ; 
        return this.loadBalancer.execute (serviceName ,
            requestFactory . createRequest(request, body, execution));
    }
}

主要的邏輯在  intercept 中,執行交給了  LoadBalancerClient 來處理,透過  LoadBalancer RequestFactory 來構建一個  LoadBalancerRequest 物件,如下面程式碼所示。

public LoadBalancerRequest<ClientHttpResponse> createRequest(final
        HttpRequest request, final byte[] body,
            final ClientHttpRequestExecution execution) {
    return new LoadBalancerRequest<ClientHttpResponse>() {
        @Override
        public ClientHttpResponse apply(final ServiceInstance instance)
                throws Exception {
            HttpRequest serviceRequest = new 
                ServiceRequestwrapper(request, instance, loadBalancer);
            if (transformers != nu11) {
                for(LoadBalancerRequestTransformer transformer :
                        transformers) {
                    serviceRequest =
                            transformer . transformRequest(serviceRequest,
                                instance);
                }
            }
            return execution. execute ( serviceRequest, body) ;
        }
    };
}

createRequest 中透過  ServiceRequestWrapper 來執行替換URI的邏輯, ServiceRequest Wrapper 中將 URI 的獲取交給了 org.springframework.cloud.client.loadbalancer.LoadBalancer Client#reconstructURI 方法。

以上就是整個  RestTemplate 結合  @LoadBalanced 的執行流程,至於具體的實現大家可以自己去研究,這裡只是介紹原理及整個流程。

四、Ribbon API 使用

當你有一些特殊的需求,想透過 Ribbon 獲取對應的服務資訊時,可以使用  LoadBalancer Client 來獲取,比如你想獲取一個  fsh-house 服務的服務地址,可以透過  LoadBalancerClient 的 choose 方法來選擇一個:

@Autowiredprivate LoadBalancerClient loadBalancer;@GetMapping("/choose")public object chooseUrl() {
    ServiceInstance instance = loadBalancer.choose("fsh-house");
    return instance;
}

訪問介面,可以看到返回的資訊如下:

{
    serviceId: "fsh-house",
    server: {
        host: "localhost",
        port: 8081,
        id: "localhost:8081",
        zone: "UNKNOWN",
        readyToServe: true,
        alive: true,
        hostPort: "localhost:8081",
        metaInfo: {
            serverGroup: null,
            serviceIdForDiscovery: null,
            instanceId: "localhost:8081",
            appName: null
        }
    },
    secure: false,
    metadata:
    { },
    host: "localhost",
    port: 8081,
    uri: "
}

五、Ribbon 飢餓載入

筆者從網上看到很多部落格中都提到過的一個情況: 在進行服務呼叫的時候,如果網路情況不好,第一次呼叫會超時。有很多大神對此提出瞭解決方案,比如把超時時間改長一點、禁用超時等。 Spring Cloud 目前正在高速發展中,版本更新很快,我們能發現的問題基本上等新版本出來的時候就都修復了,或者提供了最優的解決方案。

這個超時的問題也是-樣,Ribbon 的客戶端是在第一次請求的時候初始化的,如果超時時間比較短的話,初始化 Client 的時間再加上請求介面的時間,就會導致第一次請求超時。

透過配置  eager-load 來提前初始化客戶端就可以解決這個問題。

ribbon.eager-load.enabled = true ribbon
eager-load.clients = fsh-house
  • ribbon.eager-load.enabled :開啟 Ribbon 的飢餓載入模式。
  • ribbon.eager-load.clients :指定需要飢餓載入的服務名,也就是你需要呼叫的服務,若有多個則用逗號隔開。

怎麼進行驗證呢?網路情況確實不太好模擬,我們就透過日誌輸出來判斷吧。先不配置 eager-load,在第一次呼叫的時候會有日誌輸出,內容如下:

今日教學:RestTemplate 結合 Ribbon 使用

在  fsh-house 的 client 初始化資訊輸出的基礎上再加上  eager-load 的日誌重啟服務,就可以發現啟動的時候會輸出上面的日誌,這就證明我們的 client 已經提前初始化好了。

喜歡這篇文章的朋友們可以關注個人簡介中的公眾號

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69964492/viewspace-2767015/,如需轉載,請註明出處,否則將追究法律責任。

相關文章