Feign详解与使用
一、什么是Feign
1、概念
学过了Ribbon和Hystrix的读者都知道,Ribbon实现客户端负载均衡是通过拦截RestTemplate进行相应的处理。而RestTemplate已经对Http请求进行了一系列模板式的封装,我们使用的时候,通常会基于RestTemplate进行封装,形成相应的业务接口。
但是,这个封装业务接口的过程,需要一定的工作量;如果能有声明式的调用那么就能在开发中大大减少开发量。于是,出现了Feign,这是基于Netflix Feign开源组件进行适配SpringMVC,添加了注解支持(一些地方会有小许差别)的组件,该组件即有客户端负载均衡的功能,也有熔断器的服务降级等功能。
在我看来,它最大的好处是基于声明式开发方式,能够使得代码优雅、简洁,开发量小。
2、实现依赖
上面,我们说了,它拥有客户端负载均衡、熔断器的服务降级等功能,那么,它是怎么实现的呢?其实,它是将Ribbon和Hystrix作为底层支持,进行高层封装,并且整合SpringMVC等功能实现的一个组件。
- Ribbon
- Hystrix
以上两个组件就是Feign的实现依赖。
二、Feign的使用
接下来,讲讲Feign的使用。
- 引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 启动类注册Bean
//添加这个注解
@EnableFeignClients
上面我一直在强调,Feign是声明式开发,接下来,就展示一下使用Feign的声明式开发究竟有多少优雅、简洁。
- 基于Feign的声明式开发可以分为如下三步:
- 创建业务消费接口
- 根据相应的消费接口进行参数的绑定
- 调用业务接口
创建业务接口:
@FeignClient(value = "provider")
public interface FeignService {
/**
* to server
*
* @return s
*/
@GetMapping("/zone")
String feignService();
/**
* @param id 通过参数请求
* @return 用户信息
*/
@GetMapping("/queryUserById")
User queryUserById(@RequestParam("id") String id);
}
//用户类
public class User {
private int id;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
}
//以下是在控制层调用对应的业务方法
@AutoWired
private FeignService service;
@GetMapping("/consumer")
public String test() {
return service.feignService();
}
以上的例子就是feign在微服务中的入门级别应用,当然,Feign的使用是基于Ribbon和Hystrix,那么,我们是无需对Feign进行单独配置,只需要修改对应的Ribbon和Hystrix的配置即可。如果对于那两个组件不熟悉,可以看我关于那两个组件的文章,里面讲解得足够详细。
三、Feign配置
- 客户端配置
//这是最简单的客户端指定服务配置
@FeignClient(value = "provider")
- 客户端指定fallBack(服务降级)
//指定fallBack属性的实现类
@FeignClient(value = "provider",fallback = HystrixServiceCompenent.class)
public interface FeignService {
//这里要注意,value属性名不能省略,虽然在MVC中可以,但是在feign中不行。
@GetMapping(value="/zone")
String feignService();
@GetMapping(value="/queryUserById")
User queryUserById(@RequestParam String id);
}
//以下就是实现服务降级的组件。
@Component
public class HystrixServiceCompenent implements FeignService {
@Override
public String feignService() {
return "sorry" + "this service is fail, by_hystrix";
}
@Override
public User queryUserById(String id) {
return null;
}
}
- 全局配置
#全局配置是最为简单的,如配置ribbon
ribbon.<key>=<value>
#example
ribbon.ConnectTimeout=500
ribbon.ReadTimeout=5000
#以下是hystrix的全局配置
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
#关闭Hystrix
feign.hystrix.enabled=false
- 指定服务实例配置
#指定服务进行配置,需要指定服务名
<service>.ribbon.<key>=<value>
#example
provider.ribbon.ConnectTimeout=500
- 重试机制(Retry)(这是ribbon组件)
#更换实例后的重试次数为2
provider.ribbon.MaxAutoRetrikesNextServer=1
#重试策略首先访问首选服务一次,失败后再重试一次
provider.ribbon.MaxAutoRetries=1
上面的参数计算重试的次数:MaxAutoRetries+MaxAutoRetriesNextServer+(MaxAutoRetries *MaxAutoRetriesNextServer)=3次重试;那么包括第一次,则为4次。
注意:Hystrix的超时时间必须大于Ribbon的超时时间,否则触发了Hystrix的熔断命令之后,重试机制已经失效。即,运行过程应该是先进行请求,如果ribbon超时,则进行重试,如果再超时,超过Hystrix超时时间,就会触发熔断,从而进行服务降级等一系列操作。
- 局部禁用hystrix(有时候不希望全局禁用,比如跨zone调用服务的时候)
/**
* @author linxu
* @date 2019/7/16
* 用于禁用某个Feign客户端的Hystrix组件
* 如果想要开启,这个@Configuration注解必须注释掉,否则会出现不生效的情况。
*/
@Configuration
public class DisableHystrixConfiguration {
@Bean
@Scope("prototype")
public Feign.Builder feignBuilder(){
return Feign.builder();
}
}
//在客户端中,只需要指定配置就达到禁用的效果。
@FeignClient(value = "provider",configuration = DisableHystrixConfiguration.class)
- 请求压缩
#开启压缩
feign.compression.request.enabled=true
feign.compression.response.enabled=true
#设置压缩参数以下两个都是默认值
feign.compression.request.mime-type=text/xml,application/xml,application/json
feign.compression.request.min-request-size=2048
- Hystrix的配置更改
#在Hystrix中,我们采用了声明式配置,通过注解进行配置的修改;但是,Feign在取消了这一做法,改为配置文件配置;如下,是线程池的配置。
hystrix:
command:
default:
execution:
isolation:
strategy: THREAD
threadpool:
default:
coreSize: 20
#这个是feign客户端的名称
<client-Name>:
coreSize: 10
四、Feign必须了解的源码
- 不要使用Feign中的Retryer(使用ribbon的即可)
//可以自己实现这个,实现自己的Retryer.它的默认实现为5次重试。但是现在已经默认关闭,为了不与ribbon重叠
public interface Retryer extends Cloneable {
public static class Default implements Retryer {
private final int maxAttempts;
private final long period;
private final long maxPeriod;
int attempt;
long sleptForMillis;
//可以看到,默认为5次重试
public Default() {
this(100, SECONDS.toMillis(1), 5);
}
public Default(long period, long maxPeriod, int maxAttempts) {
this.period = period;
this.maxPeriod = maxPeriod;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
// visible for testing;
protected long currentTimeMillis() {
return System.currentTimeMillis();
}
//重试规则;超过次数,则通过抛出重试异常,取消尝试。
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;
}
long nextMaxInterval() {
long interval = (long) (period * Math.pow(1.5, attempt - 1));
return interval > maxPeriod ? maxPeriod : interval;
}
@Override
public Retryer clone() {
return new Default(period, maxPeriod, maxAttempts);
}
}
//这里就是默认关闭feign中的retry。官方都关了,我们就没有必要去开启了。
Retryer NEVER_RETRY = new Retryer() {
@Override
public void continueOrPropagate(RetryableException e) {
throw e;
}
@Override
public Retryer clone() {
return this;
}
};
}
- OKToRetryOnAllOperations选项带来的幂等问题
//基于负载均衡策略对请求进行封装。
public class FeignLoadBalancer extends
AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
private final RibbonProperties ribbon;
//获取对应ribbon的一个超时设置
protected int connectTimeout;
protected int readTimeout;
protected IClientConfig clientConfig;
protected ServerIntrospector serverIntrospector;
public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
ServerIntrospector serverIntrospector) {
super(lb, clientConfig);
this.setRetryHandler(RetryHandler.DEFAULT);
this.clientConfig = clientConfig;
this.ribbon = RibbonProperties.from(clientConfig);
RibbonProperties ribbon = this.ribbon;
this.connectTimeout = ribbon.getConnectTimeout();
this.readTimeout = ribbon.getReadTimeout();
this.serverIntrospector = serverIntrospector;
}
//运行请求的执行封装
@Override
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
throws IOException {
Request.Options options;
//配置重写,则获取重写后的配置
if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options(override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout));
}
else {
options = new Request.Options(this.connectTimeout, this.readTimeout);
}
//封装响应内容
Response response = request.client().execute(request.toRequest(), options);
return new RibbonResponse(request.getUri(), response);
}
//重试的控制方法
@Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
RibbonRequest request, IClientConfig requestConfig) {
//就是这个地方;
if (this.ribbon.isOkToRetryOnAllOperations()) {
//如果为true,默认对所有方法进行包括:超时、出现异常报错都进行重试。
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
if (!request.toRequest().httpMethod().name().equals("GET")) {
//在false基础上,如果不是GET,只会进行连接超时的重试
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig);
}
else {
//在false基础上,是GET,无论是超时、异常都会进行重写。(切记,不要在get方法中写数据的增、改、删请求)
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig);
}
}
}
public RequestSpecificRetryHandler(boolean okToRetryOnConnectErrors, boolean okToRetryOnAllErrors, RetryHandler baseRetryHandler, @Nullable IClientConfig requestConfig) {
Preconditions.checkNotNull(baseRetryHandler);
this.okToRetryOnConnectErrors = okToRetryOnConnectErrors;
this.okToRetryOnAllErrors = okToRetryOnAllErrors;
this.fallback = baseRetryHandler;
if (requestConfig != null) {
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
retrySameServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetries);
}
if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
retryNextServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer);
}
}
}
以上就是feign中必须了解的两个源码实现,如果不了解这两个源码实现,feign的使用中将会遭遇很多坑。
五、Feign的注意事项
-
Feign客户端默认是关闭重试功能,因为该功能会与Ribbon的重试机制重叠。
- 建议不要开启Feign的重试机制,毫无意义。
-
使用Feign中的Ribbon重试机制,需要注意:
-
ribbon的超时时间只有小于feign.hystrix的超时时间才会重试,否则触发熔断。
-
ribbon重试的次数计算如下:
MaxAutoRetries+MaxAutoRetriesNextServer+(MaxAutoRetries *MaxAutoRetriesNextServer)
-
OkToRetryOnAllOperations选项记得关闭,否则会触发其它post、put方法的重试(不是幂等),则会出现数据问题。默认只对Get方法进行重试。
-
-
Hystrix的超时计算,应该满足如下公式:
(1 + MaxAutoRetries + MaxAutoRetriesNextServer) * Ribbon.ReadTimeout
-
默认情况下,GET方式请求无论是连接异常还是读取异常,都会进行重试,非GET方式请求,只有连接异常时,才会进行重试,避免了非幂等问题。