Feign是一種聲明式、模板化的HTTP客戶端。在Spring Cloud中使用Feign, 咱們能夠作到使用HTTP請求遠程服務時能與調用本地方法同樣的編碼體驗,開發者徹底感知不到這是遠程方法,更感知不到這是個HTTP請求,相似於Dubbo的RPC;java
在Spring Cloud環境下,Feign的Encoder只會用來編碼沒有添加註解的參數。若是你自定義了Encoder, 那麼只有在編碼obj參數時纔會調用你的Encoder。對於Decoder, 默認會委託給SpringMVC中的XHttpMessageConverter類進行解碼。只有當狀態碼不在200 ~ 300之間時ErrorDecoder纔會被調用。ErrorDecoder的做用是能夠根據HTTP響應信息返回一個異常,該異常能夠在調用Feign接口的地方被捕獲到。咱們目前就經過ErrorDecoder來使Feign接口拋出業務異常以供調用者處理。Feign在默認狀況下使用的是JDK原生的URLConnection發送HTTP請求,沒有鏈接池,可是對每一個地址會保持一個長鏈接,即利用HTTP的persistence connection 。咱們能夠用Apache的HTTP Client替換Feign原始的http client, 從而獲取鏈接池、超時時間等與性能相關的控制能力,git
Feign默認集成了Hystrix,Ribbon 本期不詳細分析Hystrix,Ribbon 具體實現,後續會詳細介紹,github
主要關注FeignClientsConfiguration類,裏面包含feign所需的大部分配置spring
@Configuration public class FeignClientsConfiguration { @Autowired private ObjectFactory<HttpMessageConverters> messageConverters; @Autowired(required = false) private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>(); @Autowired(required = false) private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>(); @Autowired(required = false) private Logger logger; @Bean @ConditionalOnMissingBean public Decoder feignDecoder() { return new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)); } @Bean @ConditionalOnMissingBean public Encoder feignEncoder() { return new SpringEncoder(this.messageConverters); } @Bean @ConditionalOnMissingBean public Contract feignContract(ConversionService feignConversionService) { return new SpringMvcContract(this.parameterProcessors, feignConversionService); } @Bean public FormattingConversionService feignConversionService() { FormattingConversionService conversionService = new DefaultFormattingConversionService(); for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) { feignFormatterRegistrar.registerFormatters(conversionService); } return conversionService; } @Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true) public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } } @Bean @ConditionalOnMissingBean public Retryer feignRetryer() { return Retryer.NEVER_RETRY; } @Bean @Scope("prototype") @ConditionalOnMissingBean public Feign.Builder feignBuilder(Retryer retryer) { return Feign.builder().retryer(retryer); } @Bean @ConditionalOnMissingBean(FeignLoggerFactory.class) public FeignLoggerFactory feignLoggerFactory() { return new DefaultFeignLoggerFactory(logger); } }
默認使用SpringDecoder,經過springmvc裏的messageConverters進行數據轉換,具體怎麼轉換或者有哪些converters,能夠查看sringmvc的converters此處不詳細將講解;
通常狀況咱們使用默認的feignDecoder就能夠,在本類中有覆蓋該類,主要爲了實現異常傳遞,個人設計思路是服務端不作異常處理直接外拋到網關層統一處理,但因爲熔斷器的緣由(具體緣由後續會提到),外拋的異常httpStatus都爲OK,只指定固定消息結構如:{exception:xxx.Exception,code:邏輯錯誤碼,message:異常描述,httpStatus:http狀態},我會在decode處對全部feign響應進行解析,若是判斷服務端爲邏輯異常,則將異常信息保存至熔斷器上下文中,待feign熔斷器流程完成以後,會經過攔截器攔截判斷當前熔斷器上下文中是否包含異常信息,如存在則拋出異常,具體實現代碼以下:express
public Object decode(final Response response, Type type) throws IOException, FeignException { Response resetResponse = null; FeignResponseAdapter responseAdpter = new FeignResponseAdapter(response); if (responseAdpter.canRead()) { List<Charset> charsets = responseAdpter.getHeaders().getAcceptCharset(); byte[] byBody = responseAdpter.extractData(); String body = StreamUtils.copyToString(new ByteArrayInputStream(byBody), Charset.forName("utf-8")); ErrorResult errorResult = HttpErrorDecoder.decode(body); if (errorResult != null) { SecurityContext securityContext = SecurityContextHystrixRequestVariable.getInstance().get(); if (securityContext != null) { securityContext.setErrorResult(errorResult); } return null; } else { resetResponse = Response.builder().body(byBody).headers(response.headers()).status(response.status()).reason(response.reason()).request(response.request()).build(); } } else { resetResponse = response; } if (isParameterizeHttpEntity(type)) { type = ((ParameterizedType) type).getActualTypeArguments()[0]; Object decodedObject = decoder.decode(resetResponse, type); return createResponse(decodedObject, resetResponse); } else if (isHttpEntity(type)) { return createResponse(null, resetResponse); } else { return decoder.decode(resetResponse, type); } }
默認狀況與decoder同樣使用SpringEncoder具體數據轉換與decode同樣,但只有經過@xxx(springmvc 註解)註解的字段才進行encode,通常使用默認就夠,apache
ErrorDecoder分析api
主要處理feign異常decode非狀態碼:200-300,404 ,404 能夠經過配置是否會經過decode,具體源碼在SynchronousMethodHandler中緩存
if (response.status() >= 200 && response.status() < 300) { if (void.class == metadata.returnType()) { return null; } else { return decode(response); } } else if (decode404 && response.status() == 404) { return decoder.decode(response, metadata.returnType()); } else { throw errorDecoder.decode(metadata.configKey(), response); }
通常咱們會在此到處理服務端邏輯異常,但須要注意一點此處拋出的自定義異常須要包裝成HystrixBadRequestException,不然斷路器會作異常數統計(具體實現後面關於熔斷器處會分析)。那這裏是否是能夠作異常傳遞的解析點?確實能夠,關於異常傳遞有兩種方案,session
方案一
服務端以正常響應下發,消費端對全部下發消息進行解析,會有部分性能損耗,但能夠忽略,個別潔癖的人就另說;併發
方案二
那就是服務端以正確的錯誤碼下發,在errordecoder處進行解析包裝成HystrixBadRequestException外拋,看似很完美,但出現極端問題,致使斷路器會一直不閉合,影響正常服務,爲何會這麼說又得說說斷路器的規則了,簡單說下HystrixBadRequestException 斷路器不會進行判斷,既不會調用斷路器閉合,也不會調用回退方法,上面的說的極端狀況好比斷路器確斷開後,大量業務邏輯異常會致使一直不閉合,影響正常服務,通常這種機率只存在高併發狀況;
feign默認有重試機制默認5次,每次會間隔1s或上次響應時間;
具體源碼以下
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(); } sleptForMillis += interval; }
因此爲何你們都說微服務須要注意冪等性;咱們也能夠去掉重試,只須要覆蓋它
代碼以下:
/** * feign 默認屏蔽重試 如需重試 new Retryer.Default() * * @return */ @Bean @ConditionalOnProperty(name = "feign.retry.enabled", matchIfMissing = false) Retryer feignRetryer() { return Retryer.NEVER_RETRY; }
feign.retry.enabled爲自定義屬性,爲知足不一樣需求服務;
生產環境咱們會有不少場景,須要傳遞一個公共參數或者固定參數好比會話傳遞,通常有兩種,要麼每一個接口去傳遞,或統一處理,這裏選擇統一傳遞,Feign有個RequestInterceptor 該接口主要實現對request進行攔截,那咱們會發現該處咱們就能夠實如今統一處理,很遺憾的是咱們的Feign是運行在hystrix開闢的線程中,因此此處咱們是拿不到主線程的任何數據,如:request,session ,ThreadLocal 等等;固然前提hystrix的策略是SEMAPHORE(具體後續會說到),通常爲了更大的挖掘服務處理能力通常選擇Thread模式,此處默認選擇的是Thread;繼續上面話題那咱們是否是就實現不了統一處理?固然非也,Hystrix 有個上下文,能夠實現線程中數據共享,大家懂得詳細後面會說到;
實現代碼以下:
@Bean public RequestInterceptor transmitAuthorizationInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate requestTemplate) { SecurityContext securityContext = SecurityContextHystrixRequestVariable.getInstance().get(); if (securityContext != null && securityContext.getCurrentPrincipal() != null) { requestTemplate.header(HttpHeaders.PROXY_AUTHORIZATION, authorizationConverter.serializePrincipal(securityContext.getCurrentPrincipal())); } } }; }
上面有說到使用HttpClient 替代URlConnection,具體配置以下
先引入feign-httpclient ,
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
設置feign.httpclient.enabled=true
具體源碼在FeignAutoConfiguration中能夠找到
@Configuration @ConditionalOnClass(ApacheHttpClient.class) @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) protected static class HttpClientFeignConfiguration { @Autowired(required = false) private HttpClient httpClient; @Bean @ConditionalOnMissingBean(Client.class) public Client feignClient() { if (this.httpClient != null) { return new ApacheHttpClient(this.httpClient); } return new ApacheHttpClient(); } }
咱們能夠看下feign-httpclient的源碼 feign-httpclient 中就一個ApacheHttpClient
源碼以下:
/* * Copyright 2015 Netflix, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package feign.httpclient; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.NameValuePair; import org.apache.http.StatusLine; import org.apache.http.client.HttpClient; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.RequestBuilder; import org.apache.http.client.utils.URIBuilder; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.http.entity.ByteArrayEntity; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.util.EntityUtils; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; import feign.Client; import feign.Request; import feign.Response; import feign.Util; import static feign.Util.UTF_8; /** * This module directs Feign's http requests to Apache's * <a href="https://hc.apache.org/httpcomponents-client-ga/">HttpClient</a>. Ex. * <pre> * GitHub github = Feign.builder().client(new ApacheHttpClient()).target(GitHub.class, * "https://api.github.com"); */ /* * Based on Square, Inc's Retrofit ApacheClient implementation */ public final class ApacheHttpClient implements Client { private static final String ACCEPT_HEADER_NAME = "Accept"; private final HttpClient client; public ApacheHttpClient() { this(HttpClientBuilder.create().build()); } public ApacheHttpClient(HttpClient client) { this.client = client; } @Override public Response execute(Request request, Request.Options options) throws IOException { HttpUriRequest httpUriRequest; try { httpUriRequest = toHttpUriRequest(request, options); } catch (URISyntaxException e) { throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", e); } HttpResponse httpResponse = client.execute(httpUriRequest); return toFeignResponse(httpResponse).toBuilder().request(request).build(); } HttpUriRequest toHttpUriRequest(Request request, Request.Options options) throws UnsupportedEncodingException, MalformedURLException, URISyntaxException { RequestBuilder requestBuilder = RequestBuilder.create(request.method()); //per request timeouts RequestConfig requestConfig = RequestConfig .custom() .setConnectTimeout(options.connectTimeoutMillis()) .setSocketTimeout(options.readTimeoutMillis()) .build(); requestBuilder.setConfig(requestConfig); URI uri = new URIBuilder(request.url()).build(); requestBuilder.setUri(uri.getScheme() + "://" + uri.getAuthority() + uri.getRawPath()); //request query params List<NameValuePair> queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name()); for (NameValuePair queryParam: queryParams) { requestBuilder.addParameter(queryParam); } //request headers boolean hasAcceptHeader = false; for (Map.Entry<String, Collection<String>> headerEntry : request.headers().entrySet()) { String headerName = headerEntry.getKey(); if (headerName.equalsIgnoreCase(ACCEPT_HEADER_NAME)) { hasAcceptHeader = true; } if (headerName.equalsIgnoreCase(Util.CONTENT_LENGTH)) { // The 'Content-Length' header is always set by the Apache client and it // doesn't like us to set it as well. continue; } for (String headerValue : headerEntry.getValue()) { requestBuilder.addHeader(headerName, headerValue); } } //some servers choke on the default accept string, so we'll set it to anything if (!hasAcceptHeader) { requestBuilder.addHeader(ACCEPT_HEADER_NAME, "*/*"); } //request body if (request.body() != null) { HttpEntity entity = null; if (request.charset() != null) { ContentType contentType = getContentType(request); String content = new String(request.body(), request.charset()); entity = new StringEntity(content, contentType); } else { entity = new ByteArrayEntity(request.body()); } requestBuilder.setEntity(entity); } return requestBuilder.build(); } private ContentType getContentType(Request request) { ContentType contentType = ContentType.DEFAULT_TEXT; for (Map.Entry<String, Collection<String>> entry : request.headers().entrySet()) if (entry.getKey().equalsIgnoreCase("Content-Type")) { Collection values = entry.getValue(); if (values != null && !values.isEmpty()) { contentType = ContentType.create(entry.getValue().iterator().next(), request.charset()); break; } } return contentType; } Response toFeignResponse(HttpResponse httpResponse) throws IOException { StatusLine statusLine = httpResponse.getStatusLine(); int statusCode = statusLine.getStatusCode(); String reason = statusLine.getReasonPhrase(); Map<String, Collection<String>> headers = new HashMap<String, Collection<String>>(); for (Header header : httpResponse.getAllHeaders()) { String name = header.getName(); String value = header.getValue(); Collection<String> headerValues = headers.get(name); if (headerValues == null) { headerValues = new ArrayList<String>(); headers.put(name, headerValues); } headerValues.add(value); } return Response.builder() .status(statusCode) .reason(reason) .headers(headers) .body(toFeignBody(httpResponse)) .build(); } Response.Body toFeignBody(HttpResponse httpResponse) throws IOException { final HttpEntity entity = httpResponse.getEntity(); if (entity == null) { return null; } return new Response.Body() { @Override public Integer length() { return entity.getContentLength() >= 0 && entity.getContentLength() <= Integer.MAX_VALUE ? (int) entity.getContentLength() : null; } @Override public boolean isRepeatable() { return entity.isRepeatable(); } @Override public InputStream asInputStream() throws IOException { return entity.getContent(); } @Override public Reader asReader() throws IOException { return new InputStreamReader(asInputStream(), UTF_8); } @Override public void close() throws IOException { EntityUtils.consume(entity); } }; } }
根據feign request 和 options 設置參數發送/響應結果,下面看下options裏面有兩個屬性
private final int connectTimeoutMillis; private final int readTimeoutMillis;
再看下execute方法裏
RequestConfig requestConfig = RequestConfig .custom() .setConnectTimeout(options.connectTimeoutMillis()) .setSocketTimeout(options.readTimeoutMillis()) .build(); requestBuilder.setConfig(requestConfig);
設置httpclient連接超時時間,
能夠看下FeignLoadBalancer類execute方法
options = new Request.Options( configOverride.get(CommonClientConfigKey.ConnectTimeout, this.connectTimeout), (configOverride.get(CommonClientConfigKey.ReadTimeout, this.readTimeout)));
DefaultClientConfigImpl 中loadDefaultValues方法
putDefaultIntegerProperty(CommonClientConfigKey.ConnectTimeout,getDefaultConnectTimeout()
目前還不能找到key是什麼,往下看
protected void putDefaultIntegerProperty(IClientConfigKey propName, Integer defaultValue) { Integer value = ConfigurationManager.getConfigInstance().getInteger( getDefaultPropName(propName), defaultValue); setPropertyInternal(propName, value); }
getDefaultPropName方法
String getDefaultPropName(String propName) { return getNameSpace() + "." + propName; }
能夠判定了他的設置方式是nameSpace.ConnectTimeout 那nameSpace是什麼
繼續看下getnameSpace方法
@Override public String getNameSpace() { return propertyNameSpace; } public static final String DEFAULT_PROPERTY_NAME_SPACE = "ribbon"; private String propertyNameSpace = DEFAULT_PROPERTY_NAME_SPACE;
好了囉嗦一大堆其實就是ribbon.ConnectTimeout這麼配置的麼,,,
不賣關子了,DefaultClientConfigImpl包含ribbon的http請求相關配置,後面會在ribbon篇詳細介紹及優化方案
到目前爲止咱們能夠看到Feign裏面默認集成了Ribbon,Hystrix 固然Hystrix是能夠被禁用的至於Ribbon你們能夠去研究研究,
如何禁用Hystrix 咱們能夠經過配置feign.hystrix.enabled=false
源碼以下:
@Configuration @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class }) protected static class HystrixFeignConfiguration { @Bean @Scope("prototype") @ConditionalOnMissingBean @ConditionalOnProperty(name = "feign.hystrix.enabled", matchIfMissing = true) public Feign.Builder feignHystrixBuilder() { return HystrixFeign.builder(); } }
咱們須要啓用FeignClient,能夠在application中註解
@EnableFeignClients(basePackages = "com.zhaoql.api.provider.client")
下面咱們建立個簡單的例子
@FeignClient(name = "spi", fallbackFactory = IndexClientFallbackFactory.class, configuration = FeignClientConfiguration.class) public interface IndexClient { @RequestMapping(value = "/index", method = RequestMethod.POST) String index(); }
@FeignClient 重要參數
name
服務提供者的application.name 做用於ribbon,ribbo會經過該name去本地緩存中找出服務提供者實例
decode404
上面有介紹是否對404被errorDecoder decode
configuration
相關配置
fallbackFactory
熔斷器回退配置
@Bean public IndexFallbackFactory indexFallbackFactory () { return new indexFallbackFactory (); } public class IndexFallbackFactory implements FallbackFactory<IndexClient> { @Override public IndexClientcreate(Throwable cause) { return new IndexClient(){ } } }
源碼github https://github.com/zhaoqilong3031/spring-cloud-samples