內容分爲openFeign大體的使用和源碼的我的解讀,裏面參考了很多其餘優秀博客做者的內容,不少地方基本算是拾人牙慧了,不過仍是順着源碼讀了一遍加深理解。html
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 是 Netflix開源的基於HTTP和TCP等協議負載均衡組件
Ribbon 能夠用來作客戶端負載均衡,調用註冊中心的服務
Ribbon的使用須要代碼裏手動調用目標服務,請參考官方示例:官方示例
Feign是Spring Cloud組件中的一個輕量級RESTful的HTTP服務客戶端。
Feign內置了Ribbon,用來作客戶端負載均衡,去調用服務註冊中心的服務。
Feign的使用方式是:使用Feign的註解定義接口,調用這個接口,就能夠調用服務註冊中心的服務。
Feign支持的註解和用法請參考官方文檔:官方文檔。
Feign自己不支持Spring MVC的註解,它有一套本身的註解。
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服務客戶端 |
目前狀況 | 維護中 | 中止維護 | 維護中 |
按照maven的依賴管理,咱們須要使用此方式進行處理
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> <version>${feign.version}</version> <scope>compile</scope> <optional>true</optional> </dependency>
application
啓動類 須要添加對應的配置:@EnableFeignClients
用於容許訪問。
spring cloud feign的默認配置:
Spring Cloud OpenFeign默認爲假裝提供如下bean(
BeanType
beanName :)ClassName
:
Decoder
feignDecoder :(ResponseEntityDecoder
包含SpringDecoder
)Encoder
feignEncoder:SpringEncoder
Logger
feignLogger:Slf4jLogger
MicrometerCapability
micrometerCapability:若是feign-micrometer
在類路徑上而且MeterRegistry
可用Contract
feignContract:SpringMvcContract
Feign.Builder
feignBuilder:FeignCircuitBreaker.Builder
Client
feignClient:若是在類路徑FeignBlockingLoadBalancerClient
上使用Spring Cloud LoadBalancer,則使用。若是它們都不在類路徑上,則使用默認的假裝客戶端。
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
更多的用法請根據網上資料或者官方文檔,下面列舉一些具體的配置或者使用方法:
若是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
這部分配置能夠直接參考官網的處理:yml相關配置表
下面爲藉助文章理解和本身看源碼的總結。整個調用過程仍是比較好理解的。由於說白了自己就是對於一次http請求的抽象和封裝而已。不過這部分用到了不少的設計模式,好比隨處可見的建造者模式和策略模式。同時這一塊的設計使用大量的包訪問結構閉包,因此要對其進行二次開發會稍微麻煩一些,可是使用反射這些屏障基本算是形同虛設了。
參考資料:掘金【【圖文】Spring Cloud OpenFeign 源碼解析】:https://juejin.cn/post/684490...
這裏主要介紹一次openFeign請求調用的流程,對於註解處理以及組件註冊的部分放到了文章的結尾部分。
Feign實例化newInstance()
+ 實例化**SyncronizedMethodHandler**以及**ParseHandlersByName**,注入到**ReflectFeign**對象。
構建Contract對象,對於請求參數進行校驗和解析
代理類SyncronizedInvocationHandler構建 requestTeamplate對象,併發送請求
當服務經過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; }
下面就上面這段代碼進行深刻的剖析。
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; } }
此方法的做用是:調用以解析連接到HTTP請求的類中的方法。
默認實例化對象爲:<font color='red'>SpringMvcContract</font>
因爲這部分涉及子父類的調用以及多個內部方法的調用而且方法內容較多,下面先介紹下**父類**的`parseAndValidateMetadata()`大體的代碼工做流程。
遍歷全部的內部方法
parseAndValidateMetadata()
內部方法爲處理註解方法和參數內容,感興趣能夠自行了解源代碼
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()); }
因爲大部分的細節處理工做由父類完成:
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; }
下面根據一個動態代理的結構圖來理解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);
建立。
feign.SynchronousMethodHandler#invoke()
請求邏輯 到了這一步,就是代理對象執行具體請求邏輯的部分了,這一部分包括建立一個請求模板,參數解析,根據參數配置client,請求編碼和請求解碼,以及攔截器等等.....涉及的內容比較多。這個小節做爲1-3這三個部分的一個分割線。
首先咱們看下整改SynchronousMethodHandler的invoke()
處理代碼邏輯:
這裏仍是比較容易理解的,最開始先過偶見一個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; } } }
下面是閱讀源碼時臨時作的部分筆記,大體瀏覽便可。
- 經過
methodHandlerMap
分發到不一樣的請求實現處理器當中默認走
SynchronousMethodHandler
處理不一樣的請求
- 構建
requestTemplate
模板- 構建
requestOptions
配置- 獲取重試器
Retry
使用while(true) 進行無線循環. 執行請求而且對於請求的template和請求參數進行decode處理
調用攔截器對於請求進行攔截處理(使用了責任鏈模式)
BasicAuthRequestInterceptor
:默認的調用權限驗證攔截FeignAcceptGzipEncodingInterceptor
gzip編碼處理開關鏈接器。用於判斷是否容許開啓gzip壓縮FeignContentGzipEncodingInterceptor
:請求報文內容gzip壓縮攔截處理器若是日誌的配置等級不爲none,進行對應日誌級別的輸出執行
client.execute()
方法,發送http請求
- 使用
response.toBuilder
對於響應內容進行構建起的處理(注意源代碼裏面標註後續版本會廢棄這種方式? 爲何要廢棄? 那裏很差)對於返回結果解碼,調用
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()
方法將會在當前線程拋出一個未受檢查的異常。
- 若是拋出異常,使用retry進行定時重試
做用是使用傳遞給方法調用的參數來建立請求模板。主要內容爲請求的各類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());
注意:
- 這部分後續的版本可能會增長更多的處理形式,一切以最新的源碼爲準。注意文章標題聲明的版本
- 對於格式化的呢絨
加密的工做是在: 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())); } } }
說完了加密,天然也要說下解碼的動做如何處理的,下面是默認的解碼接口的實現<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; }
代碼比較簡單,這裏直接展開了,若是沒有調用參數,返回默認的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); }
重試器這部分會調用一個叫作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的倍數進行重試,若是超太重試設置的最大因子數則中止重試。
當進行上面的基礎配置以後緊接着就是執行請求的發送操做了,在發送只求以前還有一步關鍵的操做:攔截器處理
這裏會遍歷事先配置的攔截器,對於請求模板作最後的處理操做。
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 }
這裏一樣截取了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的默認實現:
client對應的Default代碼邏輯:
代碼調用的核心部分,默認按照java.net的httpconnection 進行處理。使用原始的網絡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的規範描述。
因此有時候能從源碼發掘出很多的故事,挺有趣的
這個類至關於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標頭能夠被首先設置和發送,這增長了不少延遲和內存。對於大文件,不建議使用。
這一部分請閱讀4.1 部分的關於報文數據編碼和解碼的細節部份內容
至此一個基本的調用流程基本就算是完成了。
先借(偷)一張參考資料的圖來看下整個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
關於feign的注入,在此類中提供了兩種的形式:
若是不存在,此時會實例化一個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
Feign.hystrix.HystrixInvocationHandler
當中執行的invoke實際上仍是SyncronizedMethodHandler 方法
HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
內部代碼同時還使用了命令模式的命令 HystrixCommand 進行封裝。因爲不是本文重點,這裏不作擴展。
HystrixCommand 這個對象又是拿來幹嗎的?
簡介:用於包裝代碼,將執行具備潛在風險的功能(一般是指經過網絡的服務調用)與故障和延遲容忍,統計和性能指標捕獲,斷路器和隔板功能。這個命令本質上是一個阻塞命令,但若是與
observe()
一塊兒使用,它提供了一個可觀察對象外觀。實現接口:
HystrixObservable
/HystrixInvokableInfo
HystrixInvokableInfo
: 存儲命令接口的規範,子類要求實現
HystrixObservable
: 變成觀察者支持非阻塞調用
第一次總結源碼,更多的是參考網上的資料順着別人的思路本身去一點點看的。(哈哈,聞道有前後,術業有專攻)若是有錯誤歡迎指出。
不一樣於spring那複雜層層抽象,openFeign的學習和「模仿」價值更具備意義,不少代碼一眼就能夠看到設計模式的影子,比較適合本身練手和學習提升我的的編程技巧。
另外,openFeign使用了不少的包訪問結構,這對於在此基礎上二次擴展的sentianl框架是個頭疼的問題,不過好在能夠站在反射大哥的背後,直接暴力訪問。
結合源碼再回顧官方文檔提到的功能