Feign是一种声明式、模板化的HTTP客户端。在Spring Cloud中使用Feign, 我们可以做到使用HTTP请求远程服务时能与调用本地方法一样的编码体验,开发者完全感知不到这是远程方法,更感知不到这是个HTTP请求,类似于Dubbo的RPC;
在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, 从而获取连接池、超时时间等与性能相关的控制能力,
Feign默认集成了Hystrix,Ribbon 本期不详细分析Hystrix,Ribbon 具体实现,后续会详细介绍,
主要关注FeignClientsConfiguration类,里面包含feign所需的大部分配置
@Configuration
public class FeignClientsConfiguration {
@Autowired
private ObjectFactory messageConverters;
@Autowired(required = false)
private List parameterProcessors = new ArrayList<>();
@Autowired(required = false)
private List 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熔断器流程完成之后,会通过拦截器拦截判断当前熔断器上下文中是否包含异常信息,如存在则抛出异常,具体实现代码如下:
public Object decode(final Response response, Type type) throws IOException, FeignException {
Response resetResponse = null;
FeignResponseAdapter responseAdpter = new FeignResponseAdapter(response);
if (responseAdpter.canRead()) {
List 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,一般使用默认就够,
ErrorDecoder分析
主要处理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,否则断路器会做异常数统计(具体实现后面关于熔断器处会分析)。那这里是不是可以做异常传递的解析点?确实可以,关于异常传递有两种方案,
方案一
服务端以正常响应下发,消费端对所有下发消息进行解析,会有部分性能损耗,但可以忽略,个别洁癖的人就另说;
方案二
那就是服务端以正确的错误码下发,在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 ,
io.github.openfeign
feign-httpclient
设置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
* HttpClient. Ex.
*
* 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 queryParams = URLEncodedUtils.parse(uri, requestBuilder.getCharset().name());
for (NameValuePair queryParam: queryParams) {
requestBuilder.addParameter(queryParam);
}
//request headers
boolean hasAcceptHeader = false;
for (Map.Entry> 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> 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> headers = new HashMap>();
for (Header header : httpResponse.getAllHeaders()) {
String name = header.getName();
String value = header.getValue();
Collection headerValues = headers.get(name);
if (headerValues == null) {
headerValues = new ArrayList();
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 {
@Override
public IndexClientcreate(Throwable cause) {
return new IndexClient(){
}
}
}
源码github https://github.com/zhaoqilong3031/spring-cloud-samples