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