Spring Cloud openFeign學習【3.0.2版本】

Spring Cloud openFeign學習【3.0.2版本】

前言

​ 內容分爲openFeign大體的使用和源碼的我的解讀,裏面參考了很多其餘優秀博客做者的內容,不少地方基本算是拾人牙慧了,不過仍是順着源碼讀了一遍加深理解。html

openFeign 是什麼?

​ Feign是一個聲明性web服務客戶端。它使編寫web服務客戶機更加容易,要使用Feign,須要建立一個接口並對其進行註釋。它具備可插入註釋支持,包括Feign註釋和JAX-RS註釋。java

​ Feign還支持可插拔編碼器和解碼器。Spring Cloud增長了對Spring MVC註解的支持,並支持使用Spring Web中默認使用的相同HttpMessageConverters。git

​ Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker和Spring Cloud LoadBalancer,在使用Feign時提供一個負載均衡的http客戶端程序員

如何學習?

​ 框架最大的意義在於使用,其實最好的教程就是邊作邊參考官方的文檔學習。github

官方文檔目錄地址web

官方openFeign的文檔spring

應用場景?

​ 能夠看到openFeign做爲服務的調用中轉,負責服務之間的鏈接和請求轉發的操做。OpenFeign做爲編寫服務調用支持組件在spring cloud中佔有極爲重要的位置。編程

​ 和RPC的通訊框架不一樣,openFeign使用了傳統的http做爲傳輸結構。json

​ 在以往使用Ribbon的時候,服務調用一般使用的是手動調用,這須要花費大量的人工協調時間。如今經過openFeign把服務調用「本地化」。調用其餘的服務的接口API像調用本地方法同樣。這樣既不須要頻繁的改動接口,又能夠控制服務的調用,而不會致使服務提供方的變更而「失效」。設計模式

Ribbon、Feign和OpenFeign的區別

Ribbon、Feign和OpenFeign的區別

Ribbon

​ Ribbon 是 Netflix開源的基於HTTP和TCP等協議負載均衡組件

​ Ribbon 能夠用來作客戶端負載均衡,調用註冊中心的服務

​ Ribbon的使用須要代碼裏手動調用目標服務,請參考官方示例:官方示例

Feign

​ Feign是Spring Cloud組件中的一個輕量級RESTful的HTTP服務客戶端。

​ Feign內置了Ribbon,用來作客戶端負載均衡,去調用服務註冊中心的服務

​ Feign的使用方式是:使用Feign的註解定義接口,調用這個接口,就能夠調用服務註冊中心的服務。

​ Feign支持的註解和用法請參考官方文檔:官方文檔

Feign自己不支持Spring MVC的註解,它有一套本身的註解

OpenFeign

​ OpenFeign是Spring Cloud 在Feign的基礎上支持了Spring MVC的註解,如@RequesMapping等等。OpenFeign的@FeignClient能夠解析SpringMVC的@RequestMapping註解下的接口,並經過動態代理的方式產生實現類,實現類中作負載均衡並調用其餘服務。

​ 根據上面的描述,繪製以下的表格內容:

- Ribbon Feign OpenFeign
使用方式 手動調用目標服務 Feign的註解定義接口,調用接口就能夠調用註冊中心服務 能夠直接使用服務調用的方式調用對應的服務
做用 客戶端負載均衡,服務註冊中心的服務調用 客戶端負載均衡,服務註冊中心的服務調用 動態代理的方式產生實現類,實現類中作負載均衡並調用其餘服務
開發商 Netfix Spring Cloud Spring Cloud
特色 基於HTTP和TCP等協議負載均衡組件 輕量級RESTful的HTTP服務客戶端。依靠自我實現的註解進行請求處理 支持了Spring MVC的註解的輕量級RESTful的HTTP服務客戶端
目前狀況 維護中 中止維護 維護中

openFeign增長了那些功能:

  1. 可插拔的註解支持,包括Feign註解和JSX-RS註解。
  2. 支持可插拔的HTTP編碼器和解碼器。
  3. 支持Hystrix和它的Fallback。
  4. 支持Ribbon的負載均衡。
  5. 支持HTTP請求和響應的壓縮。

openFeign的client實現方替換:

  1. 可使用http client 替換,而且openFeign 提供了良好的配置,能夠支持httpclient的細節化配置。
  2. 使用okHttpClient, 能夠實現 okhttpClient 實現自定義的httpclient注入模式,可是會出現必定的問題。

使用方式:

1. 添加依賴

​ 按照maven的依賴管理,咱們須要使用此方式進行處理

<dependency>
     <groupId>org.springframework.cloud</groupId>
     <artifactId>spring-cloud-starter-openfeign</artifactId>
     <version>${feign.version}</version>
     <scope>compile</scope>
     <optional>true</optional>
</dependency>

2. 開啓註解@EnableFeignClients

application啓動類 須要添加對應的配置:@EnableFeignClients用於容許訪問。

spring cloud feign的默認配置:

Spring Cloud OpenFeign默認爲假裝提供如下bean(BeanTypebeanName :)ClassName

  • DecoderfeignDecoder :(ResponseEntityDecoder包含SpringDecoder
  • Encoder feignEncoder: SpringEncoder
  • Logger feignLogger: Slf4jLogger
  • MicrometerCapabilitymicrometerCapability:若是feign-micrometer在類路徑上而且MeterRegistry可用
  • Contract feignContract: SpringMvcContract
  • Feign.Builder feignBuilder: FeignCircuitBreaker.Builder
  • ClientfeignClient:若是在類路徑FeignBlockingLoadBalancerClient上使用Spring Cloud LoadBalancer,則使用。若是它們都不在類路徑上,則使用默認的假裝客戶端。

3. yml增長配置:

​ yml文件內部的文件內容以下:

feign:
    client:
        config:
            feignName:
                connectTimeout: 5000
                readTimeout: 5000
                loggerLevel: full
                errorDecoder: com.example.SimpleErrorDecoder
                retryer: com.example.SimpleRetryer
                defaultQueryParameters:
                    query: queryValue
                defaultRequestHeaders:
                    header: headerValue
                requestInterceptors:
                    - com.example.FooRequestInterceptor
                    - com.example.BarRequestInterceptor
                decode404: false
                encoder: com.example.SimpleEncoder
                decoder: com.example.SimpleDecoder
                contract: com.example.SimpleContract
                capabilities:
                    - com.example.FooCapability
                    - com.example.BarCapability
                metrics.enabled: false

4. 具體使用:

​ 更多的用法請根據網上資料或者官方文檔,下面列舉一些具體的配置或者使用方法:

若是openFeign的名稱發生衝突,須要使用contextId對於防止bean的名稱衝突

@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)

上下文繼承

​ 若是將FeignClient配置爲不從父上下文繼承bean,可使用下面的寫法:

@Configuration
public class CustomConfiguration{

    @Bean
    public FeignClientConfigurer feignClientConfigurer() {
        return new FeignClientConfigurer() {
            @Override
            public boolean inheritParentConfiguration() {
                return false;
            }
        };
    }
}

注意:默認狀況下feign不會對與斜槓進行編碼,若是要對斜槓編碼,須要使用以下方式:

feign.client.decodeSlash:false

日誌輸出

​ feign的默認日誌輸出等級以下:

logging.level.project.user.UserClient: DEBUG

​ 下面是日誌打印的內容:

  • NONE:默認不記錄任何日誌(默認設置)
  • BASIC:只記錄和請求以及響應時間相關的日誌信息
  • HEADERS:記錄基本信息以及請求和響應
  • FULL:記錄請求和響應的頭、主體和元數據。(全部信息記錄)

開啓壓縮

​ 能夠經過以下配置,開始http壓縮:

feign.compression.request.enabled=true
feign.compression.response.enabled=true

​ 若是須要更進一步的配置,可使用以下的形式進行配置:

feign.compression.request.enabled=true
feign.compression.request.mime-types=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048

​ 注意2048值爲壓縮請求的最小閾值,由於若是對於全部請求進行gzip壓縮,對於小文件的性能開銷要反而要更大

​ 經過下面的配置來開啓gzip壓縮(壓縮編碼爲UTF-8,默認):

feign.compression.response.enabled=true
feign.compression.response.useGzipDecoder=true

5. 附錄:

yml相關配置表:

​ 這部分配置能夠直接參考官網的處理:yml相關配置表

openFeign的源碼解讀

​ 下面爲藉助文章理解和本身看源碼的總結。整個調用過程仍是比較好理解的。由於說白了自己就是對於一次http請求的抽象和封裝而已。不過這部分用到了不少的設計模式,好比隨處可見的建造者模式和策略模式。同時這一塊的設計使用大量的包訪問結構閉包,因此要對其進行二次開發會稍微麻煩一些,可是使用反射這些屏障基本算是形同虛設了。

​ 參考資料:掘金【【圖文】Spring Cloud OpenFeign 源碼解析】:https://juejin.cn/post/684490...

feign工做流程圖

工做流程概覽

這裏主要介紹一次openFeign請求調用的流程,對於註解處理以及組件註冊的部分放到了文章的結尾部分。

  • Feign實例化newInstance()

    + 實例化**SyncronizedMethodHandler**以及**ParseHandlersByName**,注入到**ReflectFeign**對象。
  • 構建ParseHandlersByName對象,對於參數進行轉化
  • 構建Contract對象,對於請求參數進行校驗和解析

    • 實例化SpringMvcContract對象(繼承自Contract對象)
    • 調用parseAndValidateMetadata() 處理和校驗數據類型
  • 經過jdk動態代理Proxy建立動態代理對象MethodInvocationHandler,調用動態代理對象的invoke()方法
  • 代理類SyncronizedInvocationHandler構建 requestTeamplate對象,併發送請求

    • 調用create()構建請求實體對象
    • 對於請求參數進行encode()操做
    • 構建client對象,執行請求
    • 返回請求結果
  • 獲取請求結果,請求完成

詳解openFeign工做流程(重點)

1. Feign 實例化 - newInstance()

​ 當服務經過feign調用另外一個服務的時候,在Fegin.builder對象中,會調用構造器構造一個Fegin實例,下面是feign.Feign.Builder#build的代碼內容:

public Feign build() { // 構建核心組件和相關內容 Client client = Capability.enrich(this.client, capabilities); Retryer retryer = Capability.enrich(this.retryer, capabilities); List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream() .map(ri -> Capability.enrich(ri, capabilities)) .collect(Collectors.toList()); Logger logger = Capability.enrich(this.logger, capabilities); Contract contract = Capability.enrich(this.contract, capabilities); Options options = Capability.enrich(this.options, capabilities); Encoder encoder = Capability.enrich(this.encoder, capabilities); Decoder decoder = Capability.enrich(this.decoder, capabilities); InvocationHandlerFactory invocationHandlerFactory = Capability.enrich(this.invocationHandlerFactory, capabilities); QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities); // 初始化SynchronousMethodHandler.Factory工廠,後續使用該工廠生成代理對象的方法 SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger, logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding); // 請求參數解析對象以及參數處理對象。負責根據請求類型構建對應的請求參數處理器 ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); // 這裏的 ReflectiveFeign 是整個核心的部分 return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); } }

​ 執行ReflectiveFeign構建以後,會立馬執行該Fegin子類的ReflectiveFeign#newInstance()方法。

public <T> T target(Target<T> target) { return build().newInstance(target); }
這裏設計的比較巧妙。可是並非特別難以理解
下面是`ReflectiveFeign#newInstance`方法的代碼:
public <T> T newInstance(Target<T> target) { // ParseHandlersByName::apply 方法構建請求參數解析模板和驗證handler是否有效 Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); // 對於方法handler進行處理 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))); } } // 建立接口代理對象。factory在父類build方法進行初始化 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; }

​ 下面就上面這段代碼進行深刻的剖析。

2. ParseHandlersByName 參數解析處理 - apply()

ReflectiveFeign#newInstance()當中首先執行的是feign.ReflectiveFeign.ParseHandlersByName對象的aplly()方法,進行參數解析和參數解析構建器的構建。同時能夠注意到,若是發現method handler 沒有在feign中找到對應配置,會拋出IllegalStateException異常。

public Map<String, MethodHandler> apply(Target target) { // 2.1 小節進行講解 List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; // 根據請求參數的類型,實例化不一樣的請求參數構建器 if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { // form表單提交形式 buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null) { // 普通編碼形式處理 buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); } if (md.isIgnored()) { result.put(md.configKey(), args -> { throw new IllegalStateException(md.configKey() + " is not a method handled by feign"); }); } else { result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder)); } } return result; } }

2.1 Contract 方法參數註解解析和校驗 - parseAndValidateMetadata()

​ 此方法的做用是:調用以解析連接到HTTP請求的類中的方法

​ 默認實例化對象爲:<font color='red'>SpringMvcContract</font>

因爲這部分涉及子父類的調用以及多個內部方法的調用而且方法內容較多,下面先介紹下**父類**的`parseAndValidateMetadata()`大體的代碼工做流程。
  1. 檢查handler是否爲單繼承(單實現接口),而且不支持參數化類型。不然將會拋出異常
  2. 遍歷全部的內部方法

    1. 若是是靜態方法跳過當前循環
    2. 獲取method對象以及目標class,執行內部方法parseAndValidateMetadata()
    內部方法爲處理註解方法和參數內容,感興趣能夠自行了解源代碼
  3. 檢查是否爲重寫方法,若是是則拋出異常Overrides unsupported

根據上面的介紹,下面看一下具體的邏輯代碼:

public List<MethodMetadata> parseAndValidateMetadata(Class<?> targetType) { checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s", targetType.getSimpleName()); checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s", targetType.getSimpleName()); if (targetType.getInterfaces().length == 1) { checkState(targetType.getInterfaces()[0].getInterfaces().length == 0, "Only single-level inheritance supported: %s", targetType.getSimpleName()); } final Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>(); for (final Method method : targetType.getMethods()) { if (method.getDeclaringClass() == Object.class || (method.getModifiers() & Modifier.STATIC) != 0 || Util.isDefault(method)) { continue; } // 調用內部方法, 處理註解方法和參數信息 final MethodMetadata metadata = parseAndValidateMetadata(targetType, method); checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s", metadata.configKey()); result.put(metadata.configKey(), metadata); } return new ArrayList<>(result.values()); }

2.2 SpringMvcContract 方法參數註解解析和校驗

​ 因爲大部分的細節處理工做由父類完成:

public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) { processedMethods.put(Feign.configKey(targetType, method), method); // 使用父類方法獲取 MethodMetadata MethodMetadata md = super.parseAndValidateMetadata(targetType, method); RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class); if (classAnnotation != null) { // produces - use from class annotation only if method has not specified this // produces - 只有當方法未指定時才從類註釋產生 if (!md.template().headers().containsKey(ACCEPT)) { parseProduces(md, method, classAnnotation); } // consumes -- use from class annotation only if method has not specified this // consumes - 只有當method沒有指定時才使用from類註釋 if (!md.template().headers().containsKey(CONTENT_TYPE)) { parseConsumes(md, method, classAnnotation); } // headers -- class annotation is inherited to methods, always write these if // present // headers -- 類註解被繼承到方法,若是有的話,必定要寫下來 parseHeaders(md, method, classAnnotation); } return md; }

3. 建立接口動態代理

​ 下面根據一個動態代理的結構圖來理解feign是如何完成建立接口的代理對象的。

​ 首先target就是咱們想要調用的目標服務的方法,在進過contract的註解處理以後,會交給proxy對象建立代理對象:

InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler);

​ 在這裏的第一行代碼利用工廠構建一個InvocationHandler實例,而後再使用proxy.newInstance根據代理目標方法對象的類型構建接口代理對象。

​ 而invocationHandler的構建操做由InvocationHandlerFactory工廠構建而成,而工廠的構建細節又由ReflectiveFeign.FeignInvocationHandler完成。最終返回FeignInvocationHandler 完成動態代理的後續操做。

static final class Default implements InvocationHandlerFactory { @Override public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) { return new ReflectiveFeign.FeignInvocationHandler(target, dispatch); } }

​ 建立接口代理對象以後,會執行FeignInvocationHandler 的invoke()方法,

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if ("equals".equals(method.getName())) { try { Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null; return equals(otherHandler); } catch (IllegalArgumentException e) { return false; } } else if ("hashCode".equals(method.getName())) { return hashCode(); } else if ("toString".equals(method.getName())) { return toString(); } // 經過dispatch 獲取全部方法的handler的引用,執行具體的handler方法 return dispatch.get(method).invoke(args); }

這裏涉及了一個數據結構:

Map<Method, MethodHandler> methodToHandler,也是動態代理的核心部分

MehtodHandler 是一個 LinkedHashMap的數據結構,他存儲的了全部的方法對應接口代理對象的映射。

此屬性由new ReflectiveFeign.FeignInvocationHandler(target, dispatch);建立。

3.1 接口代理對象調用feign.SynchronousMethodHandler#invoke()請求邏輯

​ 到了這一步,就是代理對象執行具體請求邏輯的部分了,這一部分包括建立一個請求模板,參數解析,根據參數配置client,請求編碼和請求解碼,以及攔截器等等.....涉及的內容比較多。這個小節做爲1-3這三個部分的一個分割線。

4. SynchronousMethodHandler動態代理對象處理詳解

​ 首先咱們看下整改SynchronousMethodHandlerinvoke()處理代碼邏輯:

​ 這裏仍是比較容易理解的,最開始先過偶見一個requestTemplate模板,同時構建請求的相關option,複製一個重試器配置給當前的線程使用。而後是核心的executeAndDecode()對於請求進行解碼和返回結果,若是整個請求執行過程出現重試異常,則嘗試調用重試器進行處理,若是重試依然失敗,則拋出未受檢查的異常或者拋出受檢查的異常。最後根據日誌的配置登記判斷日誌的打印和處理。

public Object invoke(Object[] argv) throws Throwable { // 構建請求處理模板 RequestTemplate template = buildTemplateFromArgs.create(argv); // 配置接口請求參數 Options options = findOptions(argv); // 重試器建立 Retryer retryer = this.retryer.clone(); while (true) { try { // 執行請求 return executeAndDecode(template, options); } 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; } } }

下面是閱讀源碼時臨時作的部分筆記,大體瀏覽便可。

  1. 經過methodHandlerMap 分發到不一樣的請求實現處理器當中
  2. 默認走SynchronousMethodHandler 處理不一樣的請求

    • 構建requestTemplate 模板
    • 構建requestOptions 配置
    • 獲取重試器Retry
  3. 使用while(true) 進行無線循環. 執行請求而且對於請求的template和請求參數進行decode處理

    • 調用攔截器對於請求進行攔截處理(使用了責任鏈模式)

      • BasicAuthRequestInterceptor:默認的調用權限驗證攔截
      • FeignAcceptGzipEncodingInterceptor gzip編碼處理開關鏈接器。用於判斷是否容許開啓gzip壓縮
      • FeignContentGzipEncodingInterceptor:請求報文內容gzip壓縮攔截處理器
    若是日誌的配置等級不爲none,進行對應日誌級別的輸出
  4. 執行 client.execute() 方法,發送http請求

    • 使用response.toBuilder 對於響應內容進行構建起的處理(注意源代碼裏面標註後續版本會廢棄這種方式? 爲何要廢棄? 那裏很差
  5. 對於返回結果解碼,調用AsyncResponseHandler.handlerResponse對於結果進行處理

    • 這裏的判斷邏輯比較多,判斷的順序以下:

      • 若是返回類型爲Response.class
      • 若是Body內容爲null,執行complete調用

這裏使用了CompletableFuture 異步調用處理執行結果。保證整個處理過程是異步執行而且返回的

  • CompletableFuture.complete()、
  • CompletableFuture.completeExceptionally 只能被調用一次須要注意。
若是長度爲空或者長度超過 **緩存結果最大長度。**須要設置` shouldClose`爲**false**,而且一樣執行complete調用
  • 若是返回狀態大於200而且小於300

    • 若是是void返回類型,直接調用complete
    • 不然對於返回結果進行解碼,是否須要關閉根據解碼以後的結果狀態決定(沒看懂)
    • 若是是404 而且返回值不爲void,則錯誤處理方法
    • 若是上述都不知足,根據返回結果的錯誤信息封裝錯誤結果,而且根據錯誤結果構建錯誤對象。最後經過:resultFuture.completeExceptionally 進行處理
特殊處理:若是上面的全部判斷出現異常信息,除開io異常須要二次封裝處理以外,都會觸發默認的comoleteExceptionally 方法拋出一個終止異步線程的調用.

​ + 驗證任務是否完成,若是沒有完成任務,調用 resultFuture.join() 方法將會在當前線程拋出一個未受檢查的異常。

  1. 若是拋出異常,使用retry進行定時重試

4.1 構建RequestTemplate模板

​ 做用是使用傳遞給方法調用的參數來建立請求模板。主要內容爲請求的各類url處理包括參數處理,url參數處理,對於迭代參數進行展開等等操做。這部分細節處理比較多,因爲篇幅有限這裏挑重點講一下:RequestTemplate template = resolve(argv, mutable, varBuilder);這個方法,這裏會根據事先定義的參數處理器處理參數,具體的代碼以下:

RequestTemplate resolve(Object[] argv, RequestTemplate mutable, Map<String, Object> variables) { return mutable.resolve(variables); }

​ 內部調用的是mutable對象的resolve方法,那麼它又是如何處理請求的呢?

根據不一樣的參數請求模板進行處理:

​ feign經過不一樣的參數請求模板提供多樣化的參數請求處理。 下面先看一下具體的構造圖:

​ 這裏很明顯使用了策略模式,代碼先根據參數找到具體的參數請求處理對象對於參數進行自定義的處理,在處理完成以後,調用super.resolve()進行其餘內容統一處理(模板方法)。設計的十分優秀而且巧妙,下面是對應的方法簽名:

`feign.RequestTemplate#resolve(java.util.Map<java.lang.String,?>)`

這裏可能會有疑問,這個BuildTemplateByResolvingArgs是在哪裏被初始化的?

BuildTemplateByResolvingArgs buildTemplate; // 根據請求參數的類型,實例化不一樣的請求參數構建器 if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { // form表單提交形式 buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null) { // 普通編碼形式處理 buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { // 使用默認的處理模板 buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); }

解答:其實早在第二步ParseHandlersByName這一步就對於整個請求處理模板進行確認,同時代理對象也會沿用此處理模板保證請求的冪等性.

請求參數處理細節對比:

​ 若是是form表單提交的參數:

Map<String, Object> formVariables = new LinkedHashMap<String, Object>(); for (Entry<String, Object> entry : variables.entrySet()) { if (metadata.formParams().contains(entry.getKey())) { formVariables.put(entry.getKey(), entry.getValue()); } }

​ 若是form格式,通常會將map轉爲formVariables 的格式,注意內部使用的是linkedhashmap進行處理的

若是是Body的處理方式:

Object body = argv[metadata.bodyIndex()]; checkArgument(body != null, "Body parameter %s was null", metadata.bodyIndex());

注意:

  1. 這部分後續的版本可能會增長更多的處理形式,一切以最新的源碼爲準。注意文章標題聲明的版本
  2. 對於格式化的呢絨
關於報文數據編碼和解碼的細節:

​ 加密的工做是在: requestTemplate當中完成的,而且是在BuildTemplateByResolvingArgs#resolve中進行處理,根據不一樣的請求參數類型進行細微的加密操做調整,可是代碼基本相似.

​ 下面是Encoder接口的默認實現:

class Default implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { if (bodyType == String.class) { template.body(object.toString()); } else if (bodyType == byte[].class) { template.body((byte[]) object, null); } else if (object != null) { throw new EncodeException( format("%s is not a type supported by this encoder.", object.getClass())); } } }
  1. 若是是字符串類型,則調用對象的tostring 方法
  2. 若是是字節數組則轉爲字節數組進行存儲
  3. 若是對象爲空,則拋出加密encode異常

說完了加密,天然也要說下解碼的動做如何處理的,下面是默認的解碼接口的實現<font color='gray'>(注意父類是StringDecoder而不是Decoder)</font>:

public class Default extends StringDecoder { @Override public Object decode(Response response, Type type) throws IOException { // 這裏的硬編碼感受挺突兀的,不知道是否爲設計有失誤仍是單純程序員偷懶。 // 比較傾向於加入 if(response == null ) return null; 這一段代碼 if (response.status() == 404 || response.status() == 204) return Util.emptyValueOf(type); if (response.body() == null) return null; if (byte[].class.equals(type)) { return Util.toByteArray(response.body().asInputStream()); } return super.decode(response, type); } }

這裏很奇怪竟然用了硬編碼的形式。(老外編碼老是十分自由)當返回狀態爲404或者204的時候。則根據對象的數據類型構建相關的數據類型默認值,若是是對象則返回一個空對象

  • 204編碼表明瞭空文件的請求
  • 200表明成功響應請求

​ 最後一行表示若是類型都不符合狀況下使用父類 StringDecoder 字符串的類型解碼的操做,若是字符串沒法解碼,則拋出異常信息。感興趣能夠看下StringDecoder#decode()的實現細節,這裏再也不展現。

若是發生錯誤,如何對錯誤信息進行編碼?
public Exception decode(String methodKey, Response response) { FeignException exception = errorStatus(methodKey, response); Date retryAfter = retryAfterDecoder.apply(firstOrNull(response.headers(), RETRY_AFTER)); if (retryAfter != null) { return new RetryableException( response.status(), exception.getMessage(), response.request().httpMethod(), exception, retryAfter, response.request()); } return exception; }
  1. 根據錯誤信息和方法簽名,構建異常對象
  2. 使用重試編碼進行返回請求頭的處理動做,開啓失敗以後的稍後重試操做
  3. 若是稍後重試失敗,則拋出相關異常
  4. 返回異常信息

4.2 option配置獲取

​ 代碼比較簡單,這裏直接展開了,若是沒有調用參數,返回默認的option陪孩子,不然按照制定條件構建Options配置

Options findOptions(Object[] argv) { if (argv == null || argv.length == 0) { return this.options; } return Stream.of(argv) .filter(Options.class::isInstance) .map(Options.class::cast) .findFirst() .orElse(this.options); }

4.3 構建重試器

​ 重試器這部分會調用一個叫作clone()的方法,注意這個clone方法是被重寫過的,使用的是默認實現的重試器。另外,我的認爲這個方法的起名容易形成誤解,我的比較傾向於構建一個叫作new Default()的構造函數。

public Retryer clone() { return new Default(period, maxPeriod, maxAttempts); }

​ 重試器比較重要的方法是關於異常以後的重試操做,下面是對應的方代碼

public void continueOrPropagate(RetryableException e) { if (attempt++ >= maxAttempts) { throw e; } long interval; if (e.retryAfter() != null) { interval = e.retryAfter().getTime() - currentTimeMillis(); if (interval > maxPeriod) { interval = maxPeriod; } if (interval < 0) { return; } } else { interval = nextMaxInterval(); } try { Thread.sleep(interval); } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); throw e; } sleptForMillis += interval; }

​ 這裏的重試間隔按照1.5的倍數進行重試,若是超太重試設置的最大因子數則中止重試。

4.4 請求發送和結果處理

​ 當進行上面的基礎配置以後緊接着就是執行請求的發送操做了,在發送只求以前還有一步關鍵的操做:攔截器處理

​ 這裏會遍歷事先配置的攔截器,對於請求模板作最後的處理操做

Request targetRequest(RequestTemplate template) { for (RequestInterceptor interceptor : requestInterceptors) { interceptor.apply(template); } return target.apply(template); }

關於日誌輸出級別的控制

​ 執行請求這部分代碼當中,會出現比較多相似下面的代碼。

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

​ 關於日誌輸出的級別根據以下的內容:

public enum Level { /** * No logging. 不進行打印,也是默認配置 */ NONE, /** * Log only the request method and URL and the response status code and execution time. 只記錄請求方法和URL以及響應狀態代碼和執行時間。 */ BASIC, /** * Log the basic information along with request and response headers. 記錄基本信息以及請求和響應頭。 */ HEADERS, /** * Log the headers, body, and metadata for both requests and responses. 記錄請求和響應的頭、主體和元數據。 */ FULL }

client發送請求(重點)

​ 這裏一樣截取了feign.SynchronousMethodHandler#executeAndDecode的部分代碼,毫無疑問最關鍵的部分是client.execute(request, options)方法。下面是對應的代碼內容:

Response response; long start = System.nanoTime(); try { response = client.execute(request, options); // ensure the request is set. TODO: remove in Feign 12 response = response.toBuilder() .request(request) .requestTemplate(template) .build(); } 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);

​ 下面是client對象的繼承結構圖:

​ 根據上面的結構圖,簡單說明client的默認實現:

  1. 請求方策略實現,定義頂層接口 client,在默認的狀況下使用Default 類做爲實現類。經過子類proxied對象實現 java.netURL請求方式。也就是說即便沒有任何的輔助三方工具,也能夠經過此方法api模擬構建http請求。
  2. 可使用okhttphttpclient 高性能實現進行替代,須要引入對應的feign接入實現。

client對應的Default代碼邏輯:

  • 構建請求URL對象HttpUrlConnection
  • 若是是Http請求對象,能夠根據條件設置ssl或者域名簽名
  • 設置http基本請求參數
  • 收集Header信息,設置GZIP壓縮編碼
  • 設置accept:*/*
  • 檢查是否開啓內部緩衝,若是設置了則按照指定長度緩衝

​ 代碼調用的核心部分,默認按照java.nethttpconnection 進行處理。使用原始的網絡IO流進行請求的處理,效率比較低下面是對應的具體實現代碼:

public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); return 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(format("Invalid status(%s) executing %s %s", status, connection.getRequestMethod(), connection.getURL())); } // 請求頭的處理 Map<String, Collection<String>> headers = new LinkedHashMap<>(); for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) { // response message if (field.getKey() != null) { headers.put(field.getKey(), field.getValue()); } } Integer length = connection.getContentLength(); if (length == -1) { length = null; } InputStream stream; // 對於狀態碼400以上的內容進行錯誤處理 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(); }

小插曲:關於reason屬性(能夠跳過)

​ 查看源代碼的時候無心間看到這裏有一個我的比較在乎的點,下面是respose中有一個叫作reason的字段:

/** * Nullable and not set when using http/2 * 做者以下說明 在http2中能夠不設置改屬性 * See https://github.com/http2/http2-spec/issues/202 */ public String reason() { return reason; }

​ 看到這一段頓時有些好奇爲何不須要設置reason,固然github上面也有相似的提問。

這個老哥是在2013年是這麼回答的,直白翻譯就是:關我卵事

然而事情沒有結束,後面又有人詳細的進行了提問

原文 i'm curious what was the logical reason for dropping the reason phrase? i was using the reason phrase as a title for messages presented to a user in the web browser client. i think most users are accustomed to such phrases, "Bad Request", "Not Found", etc. Now I will just have to write a mapping from status codes to my own reason phrases in the client. 機翻: 我很好奇,放棄"reason"這個詞的邏輯緣由是什麼? 我使用「reason」做爲在web瀏覽器客戶端向用戶呈現的消息的標題。我認爲大多數用戶習慣於這樣的短語,「錯誤請求」,「未找到」等。如今我只須要在客戶機中編寫一個從狀態代碼到我本身的理由短語的映射。

而後估計是受不了各類提問,上文的mnot五年後給出了一個明確的回答:

緣由短語——即便在HTTP/1.1中——也不能保證端到端攜帶; 實現能夠(也確實)忽略它並替換本身的值(例如,200老是「OK」,無論在網絡上發生什麼)。 考慮到這一點,再加上攜帶額外字節的開銷,將其從線路上刪除是有意義的。

爲了證明他的說法,從 >https://www.w3.org/Protocols/... w3c的網站中找到的以下的說明:

The Status-Code is intended for use by automata and the Reason-Phrase is intended for the human user. The client is not required to examine or display the Reason- Phrase. 狀態代碼用於自動機,而緣由短語用於人類用戶。客戶端不須要檢查或顯示緣由-短語。

這一段來源於Http1.1的規範描述。

因此有時候能從源碼發掘出很多的故事,挺有趣的

FeignBlockingLoadBalancerClient 做爲負載均衡使用:

​ 這個類至關於openFeign和ribbon的中轉類,將openfeign的請求轉接給ribbon實現負載均衡。到這裏會有一個疑問:client是如何作出選擇使用ribbon仍是spring cloud的呢的呢?

​ 其實仔細想一想不難理解,負載均衡確定是在spring bean初始化的時候完成的。FeignClientFactoryBean是整個實現的關鍵。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware

​ 下面是org.springframework.cloud.openfeign.FeignClientFactoryBean#getTarget方法代碼

@Override public Object getObject() **throws** Exception { return getTarget(); } /** \* @param <T> the target type of the Feign client 客戶端的目標類型 \* @return a {@link Feign} client created with the specified data and the context 指定數據或者上下文 \* information */ <T> T getTarget() { FeignContext context = applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); // 若是URL爲空,默認會嘗試使用** if (!StringUtils.hasText(url)) { if (!name.startsWith("http")) { url = "http://" + name; } else { url = name; } url += cleanPath(); // **默認使用ribbon做爲負載均衡,若是沒有找到,會拋出異常** return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url)); } if (StringUtils.hasText(url) && !url.startsWith("http")) { url = "http://" + url; } String url = this.url + cleanPath(); Client client = getOptional(context, Client.class); // 根據當前的系統設置實例化不一樣的負載均衡器 if (client != null) { if (client instanceof LoadBalancerFeignClient) { // not load balancing because we have a url,but ribbon is on the classpath, so unwrap // 不是負載平衡,由於咱們有一個url,可是ribbon在類路徑上,因此展開 client = ((LoadBalancerFeignClient) client).getDelegate(); } if (client instanceof FeignBlockingLoadBalancerClient) { // not load balancing because we have a url, but Spring Cloud LoadBalancer is on the classpath, so unwrap // 不是負載平衡,由於咱們有一個url,但Spring Cloud LoadBalancer是在類路徑上,因此展開 client = ((FeignBlockingLoadBalancerClient) client).getDelegate(); } builder.client(client); } Targeter targeter = get(context, Targeter.class); return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url)); }

​ 上面的內容描述了一個負載均衡器的初始化的完整過程。也證實了spring cloud 使用 ribbon 做爲默認的初始化,感興趣能夠全局搜索一下這一段異常,間接說明默認使用的是ribbon做爲負載均衡:

throw new IllegalStateException("No Feign Client for defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");

拓展:

​ 在feign.Client.Default#convertAndSend(),有一段以下的代碼設置

connection.setChunkedStreamingMode(8196);

​ 若是在代碼中禁用ChunkedStreamMode,與設置4096的代碼相比有什麼效果?

這樣作的結果是整個輸出都被緩衝,直到關閉爲止,這樣Content-length標頭能夠被首先設置和發送,這增長了不少延遲和內存。對於大文件,不建議使用。

答案來源:HttpUrlConnection.setChunkedStreamingMode的效果

關於編解碼的處理

​ 這一部分請閱讀4.1 部分的關於報文數據編碼和解碼的細節部份內容

至此一個基本的調用流程基本就算是完成了。

openFeign 總體調用鏈路圖

​ 先借(偷)一張參考資料的圖來看下整個openFeign的鏈路調用:

​ 下面是我的根據資料本身畫的圖:

openFeign註解處理流程

​ 咱們先看下開啓openFeign的方式註解:@EnableFeignClients

@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients {}

​ 注意這裏的一個註解@Import(FeignClientsRegistrar.class)。毫無疑問,實現的細節在FeignClientsRegistrar.class內部:

​ 剔除掉其餘的邏輯和細節,關鍵代碼在這一塊:

for (String basePackage : basePackages) { //…. registerFeignClient(registry, annotationMetadata, attributes); //…. }

​ 這裏調用了registerFeignClient註冊feign,根據註解配置掃描獲得響應的basepakage,若是沒有配置,則默認按照註解所屬類的路徑進行掃描。

​ 下面的代碼根據掃描的結果注入相關的bean信息,好比url,path,name,回調函數等。最後使用BeanDefinitionReaderUtils 對於bean的方法和內容進行注入。

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { String className = annotationMetadata.getClassName(); //bean配置 BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); validate(attributes); definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); String contextId = getContextId(attributes); definition.addPropertyValue("contextId", contextId); definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = contextId + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); // has a default, won't be null // 若是未配置會存在默認的配置 boolean primary = (Boolean) attributes.get("primary"); beanDefinition.setPrimary(primary); String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); // 註冊Bean BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }

​ 看完了基本的註冊機制,咱們再來看看Bean是如何完成自動注入的:這裏又牽扯到另外一個註解-@FeignAutoConfiguration

@FeignAutoConfiguration 簡單介紹

​ 關於feign的注入,在此類中提供了兩種的形式:

  • 若是存在HystrixFeign,則使用 HystrixTargeter 方法。
  • 若是不存在,此時會實例化一個DefaultTargeter 做爲默認的實現者

    具體的操做代碼以下:

    @Configuration(proxyBeanMethods = false) @ConditionalOnClass(name = "feign.hystrix.HystrixFeign") protected static class HystrixFeignTargeterConfiguration { @Bean // 優先使用Hystrix @ConditionalOnMissingBean public Targeter feignTargeter() { return new HystrixTargeter(); } } @Configuration(proxyBeanMethods = false) //若是不存在Hystrix,則使用默認的tagerter @ConditionalOnMissingClass("feign.hystrix.HystrixFeign") protected static class DefaultFeignTargeterConfiguration { @Bean @ConditionalOnMissingBean public Targeter feignTargeter() { return new DefaultTargeter(); } }

複習一下springboot幾個核心的註解表明的含義:

  • @ConditionalOnBean // 當給定的在bean存在時,則實例化當前Bean
  • @ConditionalOnMissingBean // 當給定的在bean不存在時,則實例化當前Bean
  • @ConditionalOnClass // 當給定的類名在類路徑上存在,則實例化當前Bean
  • @ConditionalOnMissingClass // 當給定的類名在類路徑上不存在,則實例化當前Bea

關於HystrixInvocationHandler的invoke方法:

Feign.hystrix.HystrixInvocationHandler 當中執行的invoke實際上仍是SyncronizedMethodHandler 方法

HystrixInvocationHandler.this.dispatch.get(method).invoke(args);

​ 內部代碼同時還使用了命令模式的命令 HystrixCommand 進行封裝。因爲不是本文重點,這裏不作擴展。

HystrixCommand 這個對象又是拿來幹嗎的?

簡介:用於包裝代碼,將執行具備潛在風險的功能(一般是指經過網絡的服務調用)與故障和延遲容忍,統計和性能指標捕獲,斷路器和隔板功能。這個命令本質上是一個阻塞命令,但若是與observe()一塊兒使用,它提供了一個可觀察對象外觀。

實現接口:HystrixObservable / HystrixInvokableInfo

HystrixInvokableInfo: 存儲命令接口的規範,子類要求實現

HystrixObservable: 變成觀察者支持非阻塞調用

總結

​ 第一次總結源碼,更多的是參考網上的資料順着別人的思路本身去一點點看的。(哈哈,聞道有前後,術業有專攻)若是有錯誤歡迎指出。

​ 不一樣於spring那複雜層層抽象,openFeign的學習和「模仿」價值更具備意義,不少代碼一眼就能夠看到設計模式的影子,比較適合本身練手和學習提升我的的編程技巧。

​ 另外,openFeign使用了不少的包訪問結構,這對於在此基礎上二次擴展的sentianl框架是個頭疼的問題,不過好在能夠站在反射大哥的背後,直接暴力訪問。

參考資料:

掘金博客【很是好】

關於負載均衡的介紹來源

官方文檔

結合源碼再回顧官方文檔提到的功能

在線代碼格式化

在線畫圖軟件

相關文章
相關標籤/搜索