Feign


Feign聲明式REST調用

OpenFeign是Netflix 開發的聲明式、模板化的HTTP請求客戶端,可以更加便捷、優雅地調用http api。

OpenFeign會根據帶有注解的函數信息構建出網絡請求的模板,在發送網絡請求之前,OpenFeign會將函數的參數值設置到這些請求模板中

feign主要是構建微服務消費端

只要使用OpenFeign提供的注解修飾定義網絡請求的接口類,就可以使用該接口的實例發送RESTful的網絡請求

還可以集成Ribbon和Hystrix,提供負載均衡和斷路器。

Feign

  • 英文表意為“假裝,偽裝,變形”, 是一個 Http 請求調用的輕量級框架,可以以 Java 接口注解的方式調用 Http 請求
  • 而不用像 Java 中通過封裝 HTTP 請求報文的方式直接調用
  • 通過處理注解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求
  • 這種請求相對而言比較直觀,Feign 封裝 了HTTP 調用流程,面向接口編程

項目安排

api-passenger-feign

service-valuation

<!-- 引入feign依賴 ,用來實現接口偽裝 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

添加接口,注解

  • 一般一個服務提供者,寫一個interface
  • 此處由於結合了eureka,所以name是 虛擬主機名,默認服務名,請求時 會將它解析成注冊表中的服務。
  • 不結合eureka,就是自定義一個client名字。就用url屬性指定 服務器列表。url=“http://ip:port/”
  • 此時的name作用就是創建負載均衡器,也可以添加@RequestMapping
@FeignClient(name = "service-valuation")
public interface ServiceForecast {
    @RequestMapping(value = "/forecast/single",method = RequestMethod.POST)
    public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest);
}

啟動類

@EnableFeignClients
@EnableFeignClients就像是一個開關,只有使用了該注解,OpenFeign相關的組件和配置機制才會生效。
@EnableFeignClients還可以對OpenFeign相關組件進行自定義配置

調用

@Autowired
private ServiceForecast serviceForecast;

@PostMapping("/forecast")
public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest) {

ResponseResult<ForecastResponse> result = serviceForecast.forecast(forecastRequest);

return ResponseResult.success(result.getData());
}

會向service-valuation服務的接口:/forecast/single 發送請求

自定義feign配置

feign的默認配置類是:org.springframework.cloud.openfeign.FeignClientsConfiguration

默認定義了feign使用的編碼器,解碼器等

允許使用@FeignClient的configuration的屬性自定義Feign配置

自定義的配置優先級高於上面的FeignClientsConfiguration

通過權限的例子,學習feign的自定義配置

服務提供者。上述例子開放service-valuation的權限 后,訪問。

<!-- 安全認證 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 關閉csrf
        http.csrf().disable();
        // 表示所有的訪問都必須認證,認證處理后才可以正常進行
        http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated();
        // 所有的rest服務一定要設置為無狀態,以提升操作效率和性能
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
}
spring: 
    security: 
        user: 
            name: root
            password: root

有如下兩種方式:

1. 自定義配置類。
2. 增加攔截器。

自定義配置

public class FeignAuthConfiguration {
    @Bean
    public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
        return new BasicAuthRequestInterceptor("root", "root");
    }
}

在feign上加配置

@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class);

如果在配置類上添加了@Configuration注解,並且該類在@ComponentScan所掃描的包中

那么該類中的配置信息就會被所有的@FeignClient共享

最佳實踐是:不指定@Configuration注解(或者指定configuration,用注解忽略)

而是手動

@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)

攔截器

import feign.RequestInterceptor;
import feign.RequestTemplate;

public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        // TODO Auto-generated method stub
        template.header("Authorization", "Basic cm9vdDpyb290");
    }
}
feign:
    client: 
    config: 
    service-valuation: 
       request-interceptors:
           - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor

報錯401

屬性定義

說明用屬性而不是用屬性中的configuration。

public class MyBasicAuthRequestInterceptor implements RequestInterceptor {
    @Override 
    public void apply(RequestTemplate template) {
        // TODO Auto-generated method stub
        template.header("Authorization", "Basic cm9vdDpyb290");
    }
}

配置文件

feign:
    client: 
        config: 
            service-valuation: 
                request-interceptors:
                    - com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor

再次訪問Ok

擴展

指定服務名稱配置

feign:
    client: 
        config: 
            service-valuation: 
                connect-timeout: 5000
                read-timeout: 5000
                logger-level: full

通用配置

feign:
    client: 
        config: 
            default: 
                connect-timeout: 5000
                read-timeout: 5000 
                logger-level: full

屬性配置比Java代碼優先級高。也可通過配置設置java代碼優先級高。

feign:
    client: 
        default-to-properties: false

feign在方法上可以設置:@RequestMapping,@ResponseBody

方法中的參數可以設置:@RequestBody等等,Spring MVC中的注解

推薦使用yml配置方式,在yml中按 代碼提示鍵,可以看到所有配置

Feign繼承

  • 編寫通用服務接口A,接口方法上寫@RequestMapping(),此接口用於 feign
  • 服務提供者 實現上面接口A
  • 服務消費者的feign client接口 繼承A

common組件(不推薦,降低耦合度)

package com.online.taxi.common.interactor;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.online.taxi.common.dto.ResponseResult;
import com.online.taxi.common.dto.order.ForecastRequest;
import com.online.taxi.common.dto.order.ForecastResponse;

public interface CommonServiceForecast {

    @RequestMapping(value = "/forecast/single",method = RequestMethod.POST)
    public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest);

}

// 提供者
@RestController
public class ServiceForecastController implements CommonServiceForecast {

    @Override
    @PostMapping("/forecast")
    public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest) {
        // 業務邏輯
        return null;
    }
}

消費者
@FeignClient(name = "service-valuation")
public interface ServiceForecast extends CommonServiceForecast {

}

這樣服務端和客戶端就耦合了,但會方便編碼

Feign壓縮

開啟壓縮可以有效節約網絡資源,但是會增加CPU壓力,建議把最小壓縮的文檔大小適度調大一點,進行gzip壓縮

feign:
    compression:
        request:
            enabled: true
         response: #設置返回值后,接受參數要改一下。
             enabled: true

點注解進去,看看默認值

org.springframework.cloud.openfeign.encoding
/**
 * The list of supported mime types.
 */
private String[] mimeTypes = new String[] { "text/xml", "application/xml", "application/json" };

/**
 * The minimum threshold content size.
 */
private int minRequestSize = 2048; 單位是B。

也可以選擇性的進行某種類型的壓縮

feign:
    compression:
        request:
            enabled: true
        mime-types: text/xml
        min-request-size: 2048\

org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingInterceptor

方法 判斷內容是否超過配置的大小

private boolean contentLengthExceedThreshold(Collection<String> contentLength) {

    try {
        if (contentLength == null || contentLength.size() != 1) {
            return false;
        }

        final String strLen = contentLength.iterator().next();
        final long length = Long.parseLong(strLen);
        return length > getProperties().getMinRequestSize();
    }catch (NumberFormatException ex) {
        return false;
    }
}

在HTTP協議中,有Content-Length的詳細解讀

參考文章:https://www.cnblogs.com/YC-L/p/14472311.html#2754049707

Content-Length用於描述HTTP消息實體的傳輸長度the transfer-length of the message-body

在HTTP協議中,消息實體長度和消息實體的傳輸長度是有區別

比如說gzip壓縮下,消息實體長度是壓縮前的長度,消息實體的傳輸長度是gzip壓縮后的長度

一般不需要設置壓縮,如果系統流量浪費比較多,可以考慮一下。

Feign日志

4種日志類型

  • none:      不記錄任何日志,默認值
  • basic:      僅記錄請求方法,url,響應狀態碼,執行時間
  • headers:在basic基礎上,記錄header信息
  • full:       記錄請求和響應的header,body,元數據
feign:
    client: 
        config: 
            service-valuation: 
                logger-level: basic

    //上面的logger-level只對下面的debug級別日志做出響應
    logging:
        level:
            com.online.taxi.passenger.feign.ServiceForecast: debug

Feign構造多參數請求

GET多參數請求

  • 接口方法種使用 方法(@RequestParam("id") long id)
  • 用map,方法(@RequestParam Map<String , Object> map)

POST多參數請求

  • 用bean,方法(@RequestBody User bean)

Feign執行流程

  • 主程序入口添加@EnableFeignClients注解開啟對Feign Client掃描加載處理
  • 根據Feign Client的開發規范,定義接口並加@FeignClient注解
  • 當程序啟動時,會進行包掃描,掃描所有@FeignClient注解的類,並將這些信息注入Spring IoC容器中
  • 當定義的Feign接口中的方法被調用時,通過JDK的代理方式,來生成具體的RequestTemplate
  • 當生成代理時,Feign會為每個接口方法創建一個RequestTemplate對象
  • 該對象封裝了HTTP請求需要的全部信息,如請求參數名、請求方法等信息都在這個過程中確定
  • 由RequestTemplate生成Request,把這個Request交給client處理,這里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp
  • 最后Client被封裝到LoadBalanceClient類,這個類結合Ribbon負載均衡發起服務之間的調用

兩大流程

程序啟動時

  • 接口的bean實例時如何初始化的
  • 被@FeignClient修飾的接口類
  • 構建Bean

網絡調用時

  • 調用上面類的方法時如何發送網絡請求

核心組件

  • FeignClientFactoryBean是創建@FeignClient修飾的接口類Bean實例的工廠類
  • FeignContext是配置組件的上下文環境,保存着相關組件的不同實例
  • 這些實例由不同的FeignConfiguration配置類構造出來

SynchronousMethodHandler是MethodHandler的子類,可以在FeignClient相應方法被調用時發送網絡請求,然后再將請求響應轉化為函數返回值進行輸出

流程

  • 啟動時會首先進行相關的BeanDefinition的動態注冊
  • 然后當Spring容器注入相關實例時會進行實例初始化
  • 最后當feign接口類實例函數調用時會發送網絡請求

入口

自動注入

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration

開關

從開關開始@EnableFeignClients

@EnableFeignClients有三個作用

一是引入FeignClientsRegistrar

@Import(FeignClientsRegistrar.class)

在@Import注解的參數中可以填寫類名,例如@Import(Abc.class),根據類Abc的不同類型

spring容器有以下四種處理方式

  • 如果Abc類實現了ImportSelector接口,spring容器就會實例化Abc類,並且調用其selectImports方法
  • DeferredImportSelector是ImportSelector的子類,如果Abc類實現了DeferredImportSelector接口,spring容器就會實例化Abc類
  • 並且調用其selectImports方法,和ImportSelector的實例不同的是
  • DeferredImportSelector的實例的selectImports方法調用時機晚於ImportSelector的實例
  • 要等到@Configuration注解中相關的業務全部都處理完了才會調用(具體邏輯在ConfigurationClassParser.processDeferredImportSelectors方法中)
  • 如果Abc類實現了ImportBeanDefinitionRegistrar接口,spring容器就會實例化Abc類,並且調用其registerBeanDefinitions方法
  • 如果Abc沒有實現ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar等其中的任何一個,spring容器就會實例化Abc類

我們此時的FeignClientsRegistrar,屬於第三種情況

二,是指定掃描FeignClient的包信息

  • 就是指定FeignClient接口類所在的包名
  • value(),basePackages(),basePackageClasses() ,默認都為空,如果要指定,可以在注解中加

三,指定FeignClient接口類的自定義配置類

  • defaultConfiguration()
  • 看注釋:默認是:FeignClientsConfiguration,
  • clients(),羅列被@FeignClient修飾的類

FeignClientsRegistrar

  • 上面提到的org.springframework.cloud.openfeign.FeignClientsRegistrar implements ImportBeanDefinitionRegistrar
  • FeignClientsRegistrar是ImportBeanDefinitionRegistrar的子類,Spring用ImportBeanDefinitionRegistrar來動態注冊BeanDefinition
  • OpenFeign通過FeignClientsRegistrar也能實現動態注冊beanfefinition的功能
  • 處理@FeignClient修飾的FeignClient接口類,將這些接口類的BeanDefinition注冊到Spring容器中
  • 這樣就可以使用@Autowired等方式來自動裝載這些FeignClient接口類的Bean實例

BeanDefinition

Spring使用BeanDefinition來描述bean

BeanDefinitionBuilder是Builder模式的應用。通過這個類我們可以方便的構建BeanDefinition的實例對象
建造者模式:https://www.runoob.com/design-pattern/builder-pattern.html

其實就是將Bean的定義信息存儲到這個BeanDefinition相應的屬性中,后面對Bean的操作就直接對BeanDefinition進行

例如拿到這個BeanDefinition后,可以根據里面的類名、構造函數、構造函數參數,使用反射進行對象創建

打斷點可以,看到啟動的時候執行到這個方法

FeignClientsRegistrar.class

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 從開關EnableFeignClients的屬性值來構建Feign的自定義Configuration進行注冊。看其代碼的第一句。
    registerDefaultConfiguration(metadata, registry);
    // 注冊被@FeignClient的修飾的接口類的信息。
    registerFeignClients(metadata, registry);
}

兩個功能

  • 注冊@EnableFeignClients提供的自定義配置類中的相關bean
  • 此時的配置類是被 @Configuration注解修飾的配置類,它會提供一系列組裝FeignClient的各類組件實例,比如Decoder、Encoder等。
  • 根據@EnableFeignClients提供的包信息掃描@FeignClient修飾的接口類,並注冊

registerDefaultConfiguration方法

點第一個方法進去,registerDefaultConfiguration

private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    // 獲取@EnableFeignClients中屬性鍵值對。
    Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
    // 如果@EnableFeignClients,注解中有屬性,並且包含defaultConfiguration,則進入此邏輯。
    if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
        String name;
        if (metadata.hasEnclosingClass()) {
            name = "default." + metadata.getEnclosingClassName();
        }
        else {
            name = "default." + metadata.getClassName();
        }
        registerClientConfiguration(registry, name,
        defaultAttrs.get("defaultConfiguration"));
    }
}

name是:default.com.online.taxi.passenger.ApiPassengerApplication

點進去registerClientConfiguration,此方法進行BeanDefinitionRegistry注冊

private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
    // 先生成beanDefinition。
    BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);

    builder.addConstructorArgValue(name);
    builder.addConstructorArgValue(configuration);
    // 注冊beandefinition
    registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition());
}

上面方法第一個參數:BeanDefinitionRegistry是Spring框架中用於動態注冊BeanDefinition信息的接口

調用其registerBeanDefinition方法可以將BeanDefinition注冊到Spring容器中

此方法第一個參數是beanName,name屬性就是注冊BeanDefinition的名稱(default.com.online.taxi.passenger.ApiPassengerApplication)

FeignClientSpecification

class FeignClientSpecification implements NamedContextFactory.Specification

FeignClientSpecification持有自定義配置類提供的組件實例,供OpenFeign使用

Spring Cloud框架使用NamedContextFactory創建一系列的運行上下文(ApplicationContext)

  • 來讓對應的Specification在這些上下文中創建實例對象
  • 這樣使得各個上下文中的實例對象相互獨立,互不影響,可以方便地通過子上下文管理一系列不同的實例對象
  • 意思就是:此處的FeignClientSpecification持有的自定義配置類的組件在feign的上下文中和其他上下文獨立
  • feign組件就是feign的組件,和其他組件區分開

NamedContextFactory有三個功能

  • 一是創建AnnotationConfigApplicationContext子上下文
  • 二是在子上下文中創建並獲取Bean實例
  • 三是當子上下文消亡時清除其中的Bean實例(通過其父類DisposableBean的destory實現)
  • 我們看NamedContextFactory的實現類有:FeignContext
  • 構造方法中有:super(FeignClientsConfiguration.class, "feign", "feign.client.name");
  • 可以看出FeignContext存儲了各類 openFeign的組件實例

此時我們發現一個類FeignContext

而FeignContext組件實例是通過:FeignAutoConfiguration自動配置的
我們看到在org.springframework.cloud.openfeign.FeignAutoConfiguration中,定義了一個bean

@Bean
public FeignContext feignContext() {
    FeignContext context = new FeignContext();
    // 此時將上面注冊的FeignClientSpecification設置到feignContext的configuration中。
    context.setConfigurations(this.configurations);
    return context;
}

看構造函數

public FeignContext() {
    super(FeignClientsConfiguration.class, "feign", "feign.client.name");
}

發現了上面所說開關中的默認配置FeignClientsConfiguration類

上面就是:將@EnableFeignClients注解中的自定義配置注冊到spring中

registerFeignClients

第二個方法,注冊feignclient接口的beanDefinition

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    ClassPathScanningCandidateComponentProvider scanner = getScanner();
    scanner.setResourceLoader(this.resourceLoader);

    Set<String> basePackages;

    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
}

注冊被@FeignClient的修飾的接口類的信息

AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);

此方法中有兩個for循環

for (String basePackage : basePackages) {
    Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
    for (BeanDefinition candidateComponent : candidateComponents) {
        // coding
    }
}

第一層循環是 項目主包,第二層循環是循環所有@FeignClient注解修飾的接口

找出來后注冊到spring,beandefinition。

FeignClientsRegistrar的registerBeanDefinitions方法主要做了兩個事情

  • 一是注冊@EnableFeignClients提供的自定義配置類中的相關Bean實例
  • 二是注冊@FeignClient注解修飾的FeignCleint接口類,然后進行Bean實例注冊

@EnableFeignClients的自定義配置類是被@Configuration注解修飾的配置類

它會提供一系列組裝FeignClient的各類組件實例

這些組件包括:Client、Targeter、Decoder、Encoder和Contract等

實例初始化

上面講了BeanDefinition注冊。下面進行實例初始化

在spring-cloud-openfeign-core-2.1.2.RELEASE中

org.springframework.cloud.openfeign.FeignClientFactoryBean

Spring容器通過調用它的getObject來獲取對應的bean實例

此時的實例是指被@FeignClient修飾的接口類的實例

點getTarget方法進去。

每個feignclient的實例都通過此工廠類,獲取對應的實例。

Client client = getOptional(context, Client.class);獲取client對象。

org.springframework.cloud.openfeign.Targeter有兩個實現類:DefaultTargeter和HystrixTargeter

DefaultTargeter

class DefaultTargeter implements Targeter {
        @Override
        public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
        FeignContext context, Target.HardCodedTarget<T> target) {
        return feign.target(target);
    }
}

其中:Feign.Builder feign

負責生成被@FeignClient修飾的接口類實例

通過Java的反射機制,生成實例

當feignclient的方法被調用時

InvocationHandler的回調函數會被調用,在回調函數中發送網絡請求

public <T> T target(Target<T> target) {
    return build().newInstance(target);
}

public Feign build() {
    SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer,ΩrequestInterceptors, logger,logLevel, decode404, closeAfterDecode, propagationPolicy);
ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory);
    return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}

ReflectiveFeign有個newInstance方法,2個功能

  • 掃描feignclient接口類的所有函數,生成對應的Handler
  • 用Proxy生成feignclient的實例對象
@SuppressWarnings("unchecked")
@Override
public <T> T newInstance(Target<T> target) {
    Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
    Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
    List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();

    for (Method method : target.type().getMethods()) {
        if (method.getDeclaringClass() == Object.class) {
        continue;
        } else 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 = factory.create(target, methodToHandler);
    T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
    new Class<?>[] {target.type()}, handler);

    for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
        defaultMethodHandler.bindTo(proxy);
    }
    return proxy;
}

此方法中apply方法作用

  • 通過Contract的parseAndValidatateMetadata方法獲得了接口類中所有方法的元數據
  • 這些信息中包含了每個方法所對應的網絡請求信息
  • 比如說請求的路徑(path)、參數(params)、頭部(headers)和body
  • 接下來apply方法會為每個方法生成一個MethodHandler
  • 此方法中factory.create作用:創建接口類的實例
  • 然后通過bindTo將InvocationHandler綁定到接口類實例上,用於處理函數調用

函數調用

在配置和實例生成結束之后,就可以直接使用FeignClient接口類的實例,調用它的函數來發送網絡請求

在調用其函數的過程中,由於設置了MethodHandler,所以最終函數調用會執行SynchronousMethodHandler的invoke方法

在該方法中,OpenFeign會將函數的實際參數值與之前生成的RequestTemplate進行結合,然后發送網絡請求

feign.SynchronousMethodHandler方法中

@Override
public Object invoke(Object[] argv) throws Throwable {
// 生成請求類似於:GET /uri HTTP/1.1
// argv:[BaseOrder(startLatitude=labore et laboris eiusmod, startLongitude=ut cupidatat, endLatitude=sit sint111, endLongitude=Excepteur Lorem reprehend)]

// template:
// POST /forecast/single HTTP/1.1
// Content-Length: 148
// Content-Type: application/json;charset=UTF-8

    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
        try {
            return executeAndDecode(template);
        } catch (RetryableException e) {
            try {
                retryer.continueOrPropagate(e);
            } catch (RetryableException th) {
                Throwable cause = th.getCause();
                if (propagationPolicy == UNWRAP && cause != null) {
                    throw cause;
                } else {
                    throw th;
                }
            }
            if (logLevel != Logger.Level.NONE) {
                logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
        }
    }

構建RequestTemplate,用RequestTemplate.Factory.create,構建url,queryMap,headerMap等

executeAndDecode點進去,有一句:response = client.execute(request, options);

  • 此時的client,就是具體發送請求的client
  • 此時發送完請求后,還會將結果封裝成Response

feign和ribbon結合的源碼

打斷點到feign.SynchronousMethodHandler的invoke第一行

feign.SynchronousMethodHandler

里面有executeAndDecode

    此代碼主要功能:構建request數據,然后通過request和options去通過LoadBalancerFeignClient.execute()方法去獲得返回值
Object executeAndDecode(RequestTemplate template) throws Throwable {
// 構建request對象,類似於:GET /uri HTTP/1.1
// request:
// POST http://service-valuation/forecast/single HTTP/1.1
// Authorization: Basic cm9vdDpyb290
// Content-Length: 148
// Content-Type: application/json;charset=UTF-8
// {"startLatitude":"labore et laboris eiusmod","startLongitude":"ut cupidatat","endLatitude":"sit sint111","endLongitude":"Excepteur Lorem reprehend"}

    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
        logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
        // 這個client就是之前構建的LoadBalancerFeignClient,是Client的實現類LoadBalancerFeignClient。
        response = client.execute(request, options);
    } catch (IOException e) {
        if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
    }
    throw errorExecuting(request, e);
    }
    long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);

    boolean shouldClose = true;
    try {
        if (logLevel != Logger.Level.NONE) {
            response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
        }
        if (Response.class == metadata.returnType()) {
            if (response.body() == null) {
                return response;
            }
            if (response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
                shouldClose = false;
                return response;
            }
            // Ensure the response body is disconnected
            byte[] bodyData = Util.toByteArray(response.body().asInputStream());
            return response.toBuilder().body(bodyData).build();
        }
        if (response.status() >= 200 && response.status() < 300) {
            if (void.class == metadata.returnType()) {
                return null;
            } else {
                Object result = decode(response);
                shouldClose = closeAfterDecode;
                return result;
            }
        } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {
            Object result = decode(response);
            shouldClose = closeAfterDecode;
            return result;
        } else {
            throw errorDecoder.decode(metadata.configKey(), response);
        }
    } catch (IOException e) {
        if (logLevel != Logger.Level.NONE) {
            logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
        }
        throw errorReading(request, response, e);
    } finally {
        if (shouldClose) {
            ensureClosed(response.body());
        }
    }
}

F5點進去execute方法

TraceLoadBalancerFeignClient.execute)走了這一行

response = super.execute(request, options);

實際就是org.springframework.cloud.openfeign.ribbon.execute

@Override
public Response execute(Request request, Request.Options options) throws IOException {
    try {
        // asUri: http://service-valuation/forecast/single
        URI asUri = URI.create(request.url());
        // clientName:service-valuation
        String clientName = asUri.getHost();
        // uriWithoutHost:http:///forecast/single
        URI uriWithoutHost = cleanUrl(request.url(), clientName);

        FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
        this.delegate, request, uriWithoutHost);

        IClientConfig requestConfig = getClientConfig(options, clientName);
        // 真正執行負載均衡的地方:
        return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
    }catch (ClientException e) {
        IOException io = findIOException(e);
        if (io != null) {
            throw io;
        }
        throw new RuntimeException(e);
    }
}

F5 進入 lbClient()

private FeignLoadBalancer lbClient(String clientName) {
    return this.lbClientFactory.create(clientName);
}

public FeignLoadBalancer create(String clientName) {
    FeignLoadBalancer client = this.cache.get(clientName);
    if (client != null) {
        return client;
    }
    IClientConfig config = this.factory.getClientConfig(clientName);
    // 獲取Ribbon ILoadBalancer信息,鼠標放到lb上,發現:我們自己配置的com.netflix.loadbalancer.RandomRule@498bbb15

    ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
    ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, ServerIntrospector.class);
    client = this.loadBalancedRetryFactory != null ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, this.loadBalancedRetryFactory) : new FeignLoadBalancer(lb, config, serverIntrospector);
    this.cache.put(clientName, client);
    return client;
}

F7繼續斷點

return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();

F5進入executeWithLoadBalancer,AbstractLoadBalancerAwareClient的下面方法:

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

    try {
        return command.submit(
            new ServerOperation<T>() {
                @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);
        }
    }
}

打斷點到:com.netflix.loadbalancer.reactive.LoadBalancerCommand的

public Observable<T> submit(final ServerOperation<T> operation) {
final ExecutionInfoContext context = new ExecutionInfoContext();

看這行代碼: (server == null ? selectServer() : Observable.just(server))
進入selectServer()。
執行到(打斷點到此行 F8)Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);

打斷點:com.netflix.loadbalancer.LoadBalancerContext

public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
String host = null;

打斷點: ILoadBalancer lb = getLoadBalancer();

打斷點:Server svc = lb.chooseServer(loadBalancerKey);

終於看到ribbon的東西了

進入chooseServer

進入if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
    logger.debug("Zone aware logic disabled or there is only one zone");
    return super.chooseServer(key);
}
來到:com.netflix.loadbalancer.BaseLoadBalancer
public Server chooseServer(Object key) {

看到了return rule.choose(key);

feign在調用其他微服務接口前,會去請求該微服務的相關信息(地址、端口等),並做一些初始化操作由於默認的懶加載特性,導致了在第一次調用時,出現超時的情況

ribbon:
    eager-load:
        enabled: true
    clients:
        - SERVICE-SMS

配置ribbon立即加載,此處需要注意的是,光配置立即加載是不生效的,還要配置客戶端列表.

總結

  • feign的使用。
  • feign的獨立使用。(大家課下實踐,feignClient(name="",url="http://ip:port/xxx"))
  • feign和ribbon結合。(配置負載均衡的地方)
  • 原理,源碼。
  • 繼承,壓縮,日志(方便開發)。

RestTemplate,自由,更貼近httpclient,方便調用別的第三方的http服務

feign,更面向對象一些,更優雅一些


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM