Feign實戰配置與詳細解析

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);
	}

}

如下就經常使用的配置進行分析

FeignDecoder分析

   默認使用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);
        }
    }

feignEncoder分析

       默認狀況與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 斷路器不會進行判斷,既不會調用斷路器閉合,也不會調用回退方法,上面的說的極端狀況好比斷路器確斷開後,大量業務邏輯異常會致使一直不閉合,影響正常服務,通常這種機率只存在高併發狀況;

feignRetryer分析

    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會話傳遞

    生產環境咱們會有不少場景,須要傳遞一個公共參數或者固定參數好比會話傳遞,通常有兩種,要麼每一個接口去傳遞,或統一處理,這裏選擇統一傳遞,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()));
                }
            }
        };
    }


   FeignHttpclient配置

        上面有說到使用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();
		}
	}

 

  如何集成Feign

    咱們須要啓用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

相關文章
相關標籤/搜索