天天看點

04、SpringCloud之Feign元件學習筆記

文章目錄

  • ​​前言​​
  • ​​一、認識Feign​​
  • ​​1.1、OpenFeign簡介​​
  • ​​二、實戰​​
  • ​​2.1、案例1:使用feign來進行遠端調用服務​​
  • ​​背景​​
  • ​​1、建立Order服務​​
  • ​​2、建立User服務(使用feign來進行遠端調用order服務)​​
  • ​​測試案例​​
  • ​​2.2、逾時配置處理​​
  • ​​逾時異常複現​​
  • ​​方案:配置逾時時長,解決報錯異常​​
  • ​​2.3、案例2:參數傳遞案例(單獨日期特别處理)​​
  • ​​常見請求體+參數案例​​
  • ​​單獨日期傳遞【特别注意】​​
  • ​​2.4、日志處理​​
  • ​​三、手寫feign簡易實作​​
  • ​​四、feign的源碼分析​​
  • ​​4.1、OpenFeign的原理分析​​
  • ​​4.2、如何掃描注解@FeignClient?​​
  • ​​4.3、如何建立代理對象去執行調用?​​
  • ​​4.4、feign調用問題快速定位​​
  • ​​五、OpenFeign總結​​
  • ​​參考文章​​

前言

本節配套案例代碼:​​Gitee倉庫​​​、​​Github倉庫​​

所有部落格檔案目錄索引:​​部落格目錄索引(持續更新)​​

學習視訊:​​動力節點最新SpringCloud視訊教程|最适合自學的springcloud+springcloudAlibaba​​

PS:本章節中部分圖檔是直接引用學習課程課件,如有侵權,請聯系删除。

一、認識Feign

在之前調用生産者的服務是使用RestTemplate,這種是HTTP請求調用,而對于服務調用的終極方案是使用OpenFeign。

1.1、OpenFeign簡介

​​openfeign文檔​​

Feign is a declarative web service client. It makes writing web service clients easier. To use Feign create an interface and annotate it. It has pluggable annotation support including Feign annotations and JAX-RS annotations. Feign also supports pluggable encoders and decoders. Spring Cloud adds support for Spring MVC annotations and for using the same HttpMessageConverters used by default in Spring Web. Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.      

是一個web用戶端,想要使用Feign隻需要使用一個接口以及注解,feign能夠支援插件式注解,并且提供編解碼器,在springcloud中也有對springmvc的注解支援,并且對于Feign本身就自帶了負載均衡器。

Feign 是一個遠端調用的元件 (接口,注解) http 調用的

Feign 內建了 ribbon ,ribbon 裡面內建了 eureka。

feign底層預設是ribbon,采用的是輪詢算法。

二、實戰

2.1、案例1:使用feign來進行遠端調用服務

背景

版本:SpringBoot:2.3.12.RELEASE;Spring-Cloud:Hoxton.SR12。

說明:建立兩個服務:order以及user。在user服務中會去使用feign來進行調用order服務。

1、建立Order服務

04、SpringCloud之Feign元件學習筆記

建立SpringBoot項目,選擇依賴如下:

04、SpringCloud之Feign元件學習筆記
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>      

①配置檔案:applicaion.yaml

server:
  port: 8081
spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka      

②在啟動器上添加開啟eurekaclient注解

@EnableEurekaClient  //開啟服務注冊      

③建立一個服務,用于對外提供服務

package com.changlu.orderservice.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.TimeUnit;

/**
 * @Description:
 * @Author: changlu
 * @Date: 6:45 PM
 */
@RestController
public class OrderController {

    @GetMapping("/doOrder")
    public String doOrder() {
        return "牛奶泡芙";
    }

}      

2、建立User服務(使用feign來進行遠端調用order服務)

04、SpringCloud之Feign元件學習筆記
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.12.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>

<dependency>            
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>      
04、SpringCloud之Feign元件學習筆記

①配置檔案:applicaion.yaml

server:
  port: 8081
spring:
  application:
    name: order-service
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka      

②在啟動器上添加開啟eurekaclient注解以及開啟openfeign注解(掃描包)

@EnableEurekaClient  //開啟服務注冊
@EnableFeignClients //開啟feign用戶端掃描      

③建立對應的feign接口(對應order服務的controller方法)

​UserOrderFeign.java​

​:

package com.changlu.userservice.feign;

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

/**
 * @Description: 遠端調用器
 * @Author: changlu
 * @Date: 6:52 PM
 */
@FeignClient(value = "order-service")//value表示服務名
public interface UserOrderFeign {

    /**
     * 你需要調用哪個controller  就寫它的方法簽名
     * 方法簽名(就是包含一個方法的所有的屬性)
     */
    @GetMapping("/doOrder")
    public String doOrder();

}      

④建立controller,進行依賴注入發起遠端調用

​UserController.java​

​:

package com.changlu.userservice.controller;

import com.changlu.userservice.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Description:
 * @Author: changlu
 * @Date: 6:50 PM
 */
@RestController
public class UserController {

    @Autowired
    private UserOrderFeign userOrderFeign;

    @GetMapping("/userDoOrder")
    public String userDoOrder() {
        System.out.println("使用者來通路接口:/userDoOrder");
        //發起遠端調用
        String res = userOrderFeign.doOrder();
        return res;
    }

}      

測試案例

我們啟動之前的eureka服務,接着啟動自己的編寫的兩個服務:

04、SpringCloud之Feign元件學習筆記
04、SpringCloud之Feign元件學習筆記

此時我們來通路:http://localhost:8082/userDoOrder

04、SpringCloud之Feign元件學習筆記

可以看到遠端調用成功!

2.2、逾時配置處理

逾時異常複現

遠端調用openfeign的預設逾時時長為1s,在目前的版本背景當中。

我們修改一下Order服務中的處理時長為2s,來看看是否會出現逾時異常:

@GetMapping("/doOrder")
public String doOrder() {
    //這裡睡眠兩秒
    try {
        TimeUnit.SECONDS.sleep(2);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    return "牛奶泡芙";
}      

接着我們重新開機下order服務,然後去通路網址:http://localhost:8082/userDoOrder

04、SpringCloud之Feign元件學習筆記

看下報錯資訊:

04、SpringCloud之Feign元件學習筆記

方案:配置逾時時長,解決報錯異常

在User服務中修改配置檔案:

04、SpringCloud之Feign元件學習筆記
ribbon:  #feign 預設調用 1s 逾時
  ReadTimeout: 3000   #修改調用時長為 5s
  ConnectTimeOut: 5000  # 修改連接配接時長為5s      

此時再通路位址:http://localhost:8082/userDoOrder

04、SpringCloud之Feign元件學習筆記

可以看到能夠得到通路結果。

2.3、案例2:參數傳遞案例(單獨日期特别處理)

常見請求體+參數案例

我們在Order服務中添加請求參數的一些請求接口:

04、SpringCloud之Feign元件學習筆記

​ParamController.java​

​:

package com.changlu.orderservice.controller;

import com.changlu.orderservice.domain.Order;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * url    /doOrder/熱幹面/add/油條/aaa
 * get傳遞一個參數
 * get傳遞多個參數
 * post傳遞一個對象
 * post傳遞一個對象+一個基本參數
 */
@RestController
public class ParamController {

    //url參數:PathVariable
    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age) {
        System.out.println(name + ":" + age);
        return "ok";
    }

    //請求參數:?xx=xx
    @GetMapping("oneParam")
    public String oneParam(@RequestParam(required = false) String name) {
        System.out.println(name);
        return "ok";
    }

    //請求參數:?xx=xx&xx=xx
    @GetMapping("twoParam")
    public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age) {
        System.out.println(name);
        System.out.println(age);
        return "ok";
    }

    //請求體參數
    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order) {
        System.out.println(order);
        return "ok";
    }

    //請求體 + 請求參數url中的xx=xx
    @PostMapping("oneObjOneParam")
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name) {
        System.out.println(name);
        System.out.println(order);
        return "ok";
    }
}      
04、SpringCloud之Feign元件學習筆記

接着我們來在user服務中添加對應的feign的接口方法,然後再UserController的某個接口方法裡來進行遠端調用:

​UserOrderFeign.java​

​·:

package com.changlu.userservice.feign;

import com.changlu.userservice.domain.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

import java.util.Date;

/**
 * @Description: 遠端調用器
 * @Author: changlu
 * @Date: 6:52 PM
 */
@FeignClient(value = "order-service")//value表示服務名
public interface UserOrderFeign {

    /**
     * 你需要調用哪個controller  就寫它的方法簽名
     * 方法簽名(就是包含一個方法的所有的屬性)
     */
    @GetMapping("/doOrder")
    public String doOrder();


    //*****請求參數測試案例*****
    //url參數:PathVariable
    @GetMapping("testUrl/{name}/and/{age}")
    public String testUrl(@PathVariable("name") String name, @PathVariable("age") Integer age);

    //請求參數:?xx=xx
    @GetMapping("oneParam")
    public String oneParam(@RequestParam(required = false) String name);

    //請求參數:?xx=xx&xx=xx
    @GetMapping("twoParam")
    public String twoParam(@RequestParam(required = false) String name, @RequestParam(required = false) Integer age);

    //請求體參數
    @PostMapping("oneObj")
    public String oneObj(@RequestBody Order order);

    //請求體 + 請求參數url中的xx=xx
    @PostMapping("oneObjOneParam")
    public String oneObjOneParam(@RequestBody Order order,@RequestParam("name") String name);

}      

​UserController.java​

​:

package com.changlu.userservice.controller;

import com.changlu.userservice.domain.Order;
import com.changlu.userservice.feign.UserOrderFeign;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;

/**
 * @Description:
 * @Author: changlu
 * @Date: 6:50 PM
 */
@RestController
public class UserController {
    @GetMapping("/testParam")
    public String testParam() {
        //第一個:url路徑攜帶參數
        String s1 = userOrderFeign.testUrl("changlu", 18);
        System.out.println(s1);

        //第二個:1個請求參數
        String s2 = userOrderFeign.oneParam("changlu");
        System.out.println(s2);

        //第三個:兩個請求參數
        String s3 = userOrderFeign.twoParam("changlu", 666);
        System.out.println(s3);

        Order order = Order.builder()
                .name("泡芙")
                .price(1000.0)
                .time(new Date())
                .id(1)
                .build();
        //第四個:請求體
        String s4 = userOrderFeign.oneObj(order);
        System.out.println(s4);

        //第五個:第一個請求體+參數
        String s5 = userOrderFeign.oneObjOneParam(order, "changlu");
        System.out.println(s5);
        return "ok";
    }
}      

測試一下:

04、SpringCloud之Feign元件學習筆記
04、SpringCloud之Feign元件學習筆記

單獨日期傳遞【特别注意】

Order服務:

//ParamController
//時間請求參數
@GetMapping("testTime")
public String testTime(@RequestParam Date date){
    System.out.println(date);
    return "ok";
}      

User服務:

//接口:UserOrderFeign
@GetMapping("testTime")
public String testTime(@RequestParam Date date);

//控制器類:UserController
/**
     * Sun Mar 20 10:24:13 CST 2022
     * Mon Mar 21 00:24:13 CST 2022  +- 14個小時
     * 1.不建議單獨傳遞時間參數
     * 2.轉成字元串(推薦,比較好的方案,在服務端可以進行format轉為date對象)   2022-03-20 10:25:55:213 因為字元串不會改變
     * 3.jdk LocalDate 年月日 沒問題    LocalDateTime 會丢失秒
     * 4.改feign的源碼
     *
     * @return
     */
@GetMapping("/testTime")
public String testTime(){
    //錯誤示例1:使用new Date();  
    userOrderFeign.testTime(new Date());
    //正确方案:服務端的請求接口不應該使用Date來進行接收,盡可能使用字元串、LocalDate來進行傳遞
    return "ok";
}      

錯誤案例Order服務列印結果(時間不準确):Sun Jul 17 10:26:20 CST 2022

2.4、日志處理

需求:若是我們想要知道每一個請求的詳細請求參數,那麼我們可以來進行打開日志配置以及來注入相應的日志等級。

操作1:在啟動器或者配置類中注入一個日志等級。

/**
     * 列印fein日志資訊 級别
     * @return
     */
@Bean
public Logger.Level level(){
    return Logger.Level.FULL;
}      

操作2:在配置檔案yaml中打開配置。

# 日志等級選擇
logging:
  level:
    com.changlu.userservice.feign.UserOrderFeign: debug  # 我需要答應這個接口下面的日志      

接着我們來測試一下:通路網址http://localhost:8082/testTime

此時就可以看到feign發起請求的一系列參數如下:

04、SpringCloud之Feign元件學習筆記

三、手寫feign簡易實作

本質:feign本質就是進行發送http請求,并且再此基礎上具備負載均衡的功能,我們來對其進行複現一下。

我們來接着二實戰中User服務中UserOrderFeign接口,來對該接口中的doOrder方法進行代理:

04、SpringCloud之Feign元件學習筆記

①首先我們來增強RestTemplate方法,另起能夠具備負載均衡的效果。

在對應的啟動器類UserserviceApplication中添加該bean:

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

②之後我們在test測試包中來進行代理類編寫

package com.changlu.userservice;

import com.changlu.userservice.controller.UserController;
import com.changlu.userservice.feign.UserOrderFeign;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

@SpringBootTest
class UserserviceApplicationTests {

    @Autowired
    private RestTemplate restTemplate;

    @Test
    void contextLoads() {
        UserOrderFeign feign = (UserOrderFeign)Proxy.newProxyInstance(UserController.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                String res = null;
                if (method.getName().equals("doOrder")) {
                    //1、擷取到對應的path路徑
                    // 能去拿到對方的ip和port 并且拿到這個方法上面的注解裡面的值 那麼就完事了
                    GetMapping annotation = method.getAnnotation(GetMapping.class);
                    String[] paths = annotation.value();
                    String path = paths[0];
                    //2、根據對應的的方法來擷取到相應的class位元組碼
                    Class<?> aclass = method.getDeclaringClass();
                    //3、擷取到對應feign類的注解,取得對應的服務名
                    FeignClient fannoation = aclass.getAnnotation(FeignClient.class);
                    String serviceName = fannoation.value();
                    //4、拼接服務位址
                    String url = "http://" + serviceName + path;
                    //5、發送請求(使用resttemplate來進行發送請求)
                    res = restTemplate.getForObject(url, String.class);
                }
                return res;
            }
        });
        System.out.println(feign.doOrder());
    }

}      
04、SpringCloud之Feign元件學習筆記

測試成功!

四、feign的源碼分析

4.1、OpenFeign的原理分析

使用動态代理jdk (invoke) 及cglib 子類繼承的 :

源碼簡述:

1、給接口建立代理對象(啟動掃描)

2、代理對象執行進入 invoke 方法

3、在 invoke 方法裡面做遠端調用

具體流程:

1、通過注解掃描來擷取到調用服務名稱以及對應的接口url。

04、SpringCloud之Feign元件學習筆記

2、拼接:``http://服務名/doOrder​

​,首先會通過ribbon從注冊中心中根據服務名擷取對應的通路位址,最終根據負載均衡政策确定得到一個服務,最終得到的對應的url位址:​

​http://ip:port/doOrder`

3、最終發起請求,來進行遠端調用。

4.2、如何掃描注解@FeignClient?

在啟動器上我們添加了一個開啟Feign掃描的注解:

@EnableFeignClients //開啟feign用戶端掃描

//看下對應的注解源碼
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})  //FeignClientsRegistrar是Feign的注冊類
public @interface EnableFeignClients {
    String[] value() default {};

    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}      

進入 ​

​FeignClientsRegistrar​

​ 這個類 去檢視裡面的東西

04、SpringCloud之Feign元件學習筆記
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    this.registerDefaultConfiguration(metadata, registry);
    //掃描注解來進行一個注冊(@FeignClient)
    this.registerFeignClients(metadata, registry);
}

//AnnotationMetadata即注解的元資訊
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
    //擷取到啟動類上的feign注解
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
    if (clients != null && clients.length != 0) {
        Class[] var12 = clients;
        int var14 = clients.length;

        for(int var16 = 0; var16 < var14; ++var16) {
            Class<?> clazz = var12[var16];
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    } else {
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<String> basePackages = this.getBasePackages(metadata);
        Iterator var8 = basePackages.iterator();
        
        //循環周遊得到對應的包名
        while(var8.hasNext()) {
            String basePackage = (String)var8.next();
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }

    Iterator var13 = candidateComponents.iterator();

    while(var13.hasNext()) {
        BeanDefinition candidateComponent = (BeanDefinition)var13.next();
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition)candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
            //拿到接口上的注解
            Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
            String name = this.getClientName(attributes);
            this.registerClientConfiguration(registry, name, attributes.get("configuration"));
            //将建立的代理對象registry交給spring
            this.registerFeignClient(registry, annotationMetadata, attributes);
        }
    }

}      

4.3、如何建立代理對象去執行調用?

在啟動時,在ReflectiveFeign的newInstance方法中,給接口建立了代理對象

public class ReflectiveFeign extends Feign {
    
    public <T> T newInstance(Target<T> target) {
        Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
        Method[] var5 = target.type().getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getDeclaringClass() != Object.class) {
                if (Util.isDefault(method)) {
                    DefaultMethodHandler handler = new DefaultMethodHandler(method);
                    defaultMethodHandlers.add(handler);
                    methodToHandler.put(method, handler);
                } else {
                    methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
                }
            }
        }

        InvocationHandler handler = this.factory.create(target, methodToHandler);
        //建立代理對象
        T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
        Iterator var12 = defaultMethodHandlers.iterator();

        while(var12.hasNext()) {
            DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
            defaultMethodHandler.bindTo(proxy);
        }

        return proxy;
    }
}      

ReflectiveFeign 類中的 invoke 方法幫我們完成調用:

04、SpringCloud之Feign元件學習筆記

當我們去進行遠端調用的時候可以debug到對應的斷點:

04、SpringCloud之Feign元件學習筆記

​SynchronousMethodHandler ​

​​的​

​invoke​

​​中給每一個請求建立了一個​

​requestTemplate​

​ 對 象,去執行請求:

final class SynchronousMethodHandler implements MethodHandler {
    
    //執行請求走對應的invoke方法
    public Object invoke(Object[] argv) throws Throwable {
        //可以看到建構了一個RequestTemplate
        RequestTemplate template = this.buildTemplateFromArgs.create(argv);
        Options options = this.findOptions(argv);
        Retryer retryer = this.retryer.clone();

        while(true) {
            try {
                //交給ribbon來進行請求調用
                return this.executeAndDecode(template, options);
            } catch (RetryableException var9) {
                ...
            }
        }
    }
}

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
        Request request = this.targetRequest(template);
        if (this.logLevel != Level.NONE) {
            this.logger.logRequest(this.metadata.configKey(), this.logLevel, request);
        }
        long start = System.nanoTime();
        Response response;
        try {
            //這裡client指的是LoadBalancerFeignClient,就是對應的負載均衡用戶端請求工具
            response = this.client.execute(request, options);
            response = response.toBuilder().request(request).requestTemplate(template).build();
        } catch (IOException var12) {
            if (this.logLevel != Level.NONE) {
                this.logger.logIOException(this.metadata.configKey(), this.logLevel, var12, this.elapsedTime(start));
            }

            throw FeignException.errorExecuting(request, var12);
        }
}      

此時就會走LoadBalancerFeignClient中的execute():

public class LoadBalancerFeignClient implements Client {
    
    public Response execute(Request request, Options options) throws IOException {
        try {
            //整理得到對應的url
            URI asUri = URI.create(request.url());
            String clientName = asUri.getHost();
            URI uriWithoutHost = cleanUrl(request.url(), clientName);
            RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
            IClientConfig requestConfig = this.getClientConfig(options, clientName);
            //執行處理根據負載均衡處理器
            return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
        } catch (ClientException var8) {
            IOException io = this.findIOException(var8);
            if (io != null) {
                throw io;
            } else {
                throw new RuntimeException(var8);
            }
        }
    }
}      

接着會走AbstractLoadBalancerAwareClient的executeWithLoadBalancer:

public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);

        try {
            return command.submit(
                new ServerOperation<T>() {
                    //此時Server對象會取得最終的ip位址,例如:ip位址:port
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            //這裡才是真正進行發送請求操作
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking()
                .single();
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
        
    }      

最終在FeignLoadBalancer中的execute方法中完成遠端方法通路:

public FeignLoadBalancer.RibbonResponse execute(FeignLoadBalancer.RibbonRequest request, IClientConfig configOverride) throws IOException {
    Options options;
    if (configOverride != null) {
        RibbonProperties override = RibbonProperties.from(configOverride);
        options = new Options((long)override.connectTimeout(this.connectTimeout), TimeUnit.MILLISECONDS, (long)override.readTimeout(this.readTimeout), TimeUnit.MILLISECONDS, override.isFollowRedirects(this.followRedirects));
    } else {
        options = new Options((long)this.connectTimeout, TimeUnit.MILLISECONDS, (long)this.readTimeout, TimeUnit.MILLISECONDS, this.followRedirects);
    }
    //最終這裡完成了遠端方法調用
    Response response = request.client().execute(request.toRequest(), options);
    return new FeignLoadBalancer.RibbonResponse(request.getUri(), response);
}      

其實本質就是發送的HttpURLConnection

04、SpringCloud之Feign元件學習筆記
public Response execute(Request request, Options options) throws IOException {
    //發起HttpURLConnection請求
    HttpURLConnection connection = this.convertAndSend(request, options);
    //轉轉響應體
    return this.convertResponse(connection, request);
}

Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
    //擷取到狀态碼
    int status = connection.getResponseCode();
    String reason = connection.getResponseMessage();
    if (status < 0) {
        throw new IOException(String.format("Invalid status(%s) executing %s %s", status, connection.getRequestMethod(), connection.getURL()));
    } else {
        Map<String, Collection<String>> headers = new LinkedHashMap();
        Iterator var6 = connection.getHeaderFields().entrySet().iterator();

        while(var6.hasNext()) {
            Entry<String, List<String>> field = (Entry)var6.next();
            if (field.getKey() != null) {
                headers.put(field.getKey(), field.getValue());
            }
        }

        Integer length = connection.getContentLength();
        if (length == -1) {
            length = null;
        }

        InputStream stream;
        if (status >= 400) {
            stream = connection.getErrorStream();
        } else {
            stream = connection.getInputStream();
        }

        return Response.builder().status(status).reason(reason).headers(headers).request(request).body(stream, length).build();
    }
}      

4.4、feign調用問題快速定位

隻要是 feign 調用出了問題,看 feign 包下面的 Client 接口下面的 108 行。

04、SpringCloud之Feign元件學習筆記

我們可以根據對應響應的狀态碼來進行定位:

200:成功         400:請求參數錯誤   401:沒有權限 
403:權限不夠     404:路徑不比對     405:方法不允許 
500:提供者報錯了  302:資源重定向      

五、OpenFeign總結

OpenFeign 主要基于接口和注解實作了遠端調用。

源碼總結:面試

1、OpenFeign 用過嗎?它是如何運作的?

在主啟動類上加上@EnableFeignClients 注解後,啟動會進行包掃描,把所有加了 @FeignClient(value=”xxx-service”)注解的接口進行建立代理對象通過代理對象,使用 ribbon 做了負載均衡和遠端調用      

2、如何建立的代理對象?

當 項 目 在 啟 動 時 , 先 掃 描 , 然 後 拿 到 标 記 了 @FeignClient 注 解 的 接 口 信 息 , 由 ReflectiveFeign 類的 newInstance 方法建立了代理對象 JDK 代理      

3、OpenFeign 到底是用什麼做的遠端調用?

使用的是 HttpURLConnection (java.net)      
  1. OpenFeign 怎麼和 ribbon 整合的?
在代理對象執行調用的時候      
04、SpringCloud之Feign元件學習筆記

參考文章