Open Feign
在前面的学习中,我们使用了Ribbon的负载均衡功能,简化了远程调用时的代码:
String user = this.restTemplate.getForObject("http://spring-provider/provider/" + id, String.class);
如果就学到这里,可能以后需要编写类似的大量重复代码,格式基本相同,无非参数不一样。有没有更优雅的方式,来对这些代码再次优化呢?这就是我们接下来要学的Feign的功能了。
简介
Netflix 提供了 Feign,并在 2016.7 月的最后一个版本 8.18.0
之后,将其捐赠给 spring cloud 社区,并更名为 OpenFeign 。OpenFeign 的第一个版本就是 9.0.0
,OpenFeign 会完全代理 HTTP 的请求,在使用过程中我们只需要依赖注入 Bean,然后调用对应的方法传递参数即可。这对程序员而言屏蔽了 HTTP 的请求响应过程,让代码更趋近于『调用』的形式。
Feign是一个远程调用组件,集成了ribbon和hystrix。把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。
入门
导入依赖
1、引入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
2、启动类增加@EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 开启feign客户端,无需配置熔断器和负载均衡注解
public class SpringConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(SpringConsumerApplication.class, args);
}
}
启动类增加了新的注解: @EnableFeignClients,如果我们要使用 Feign(声明式 HTTP 客户端),必须要在启动类加入这个注解开启 Feign 。
这样,我们的 Feign 就已经集成完成了,那么如何通过 Feign 去调用之前我们写的 HTTP 接口呢?
首先创建一个接口 ApiService(名字任意),并且通过注解配置要调用的服务地址。
Feign的客户端
@FeignClient(value = "spring-provider") // 标注该类是一个feign接口
public interface ProviderOpenfeign {
@GetMapping("/sms/provider/{id}")
String provider(@PathVariable("id") String id);
}
- 这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像;
@FeignClient
,声明这是一个Feign客户端,类似@Mapper
注解。同时通过value
属性指定服务名称;- 接口中定义的方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果;
- 一个服务只能被一个类绑定,不能让多个类绑定同一个远程服务,否则,会在启动项目是出现『已绑定』异常。
说明:
1、方法的返回值、参数以及requestmapping路径要和提供方的一模一样和消费方无关,消费方只需要调用该方法得到结果;
2、如果服务方controller类有@RequestMapping 定义命名空间,provider,在这里我们建议在方法上面拼接,不能在ProviderOpenfeign接口上去定义@requestMapping("/provider/");
3、@PathVariable("id") 这个括号里面的id不能省略,同理@Requestparam("id")里面的id也不能省略。
改造原来的调用逻辑,调用ProviderOpenfeign接口:
@Controller
public class ConsumerController {
@Autowired
private ProviderOpenfeign providerOpenfeign;
@RequestMapping("/consumerOpenfeign/{id}")
public String consumerOpenfeign(@RequestParam("id") String id){
String consumer = providerOpenfeign.provider(id);
return "consumerOpenfeign " + consumer;
}
}
Hystrix支持
OPen Feign默认也有对Hystrix的集成:
默认是关闭的,我们需要通过下面的参数来开启:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能,默认超时1s
只需要开启hystrix的熔断功能即可,默认时间是1s中,如果要修改熔断的时间,要做如下的配置:
feign:
hystrix:
enabled: true # 开启Feign的熔断功能
client:
config:
default:
connectTimeout: 4000 #设置feign超时连接时长
readTimeout: 4000 #设置feign请求的超时时长 4s之后 提供方没有响应 直接降级
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 6000
strategy: THREAD
timeout:
enabled: true
说明:
connectTimeout是feign连接某个服务的超时时间,
readTimeout是feign请求某个接口的超时时长,
timeoutInMilliseconds是hystrix熔断的时间,
Hystrix的超时时间是站在命令执行时间来看的和Feign设置的超时时间在设置上并没有关联关系。Hystrix不仅仅可以封装Http调用,还可以封装任意的代码执行片段。Hystrix是从命令对象的角度去定义,某个命令执行的超时时间,超过此此时间,命令将会直接熔断,假设hystrix 的默认超时时间设置了3000(3秒),而feign 设置的是4秒,那么Hystrix会在3秒到来时直接熔断返回,不会等到feign的4秒执行结束,如果hystrix的超时时间设置为6s,而feign 设置的是4秒,那么最终要以feign的时间4s为准,你可以理解为以时间短的为准,这两段都要配置,不能不配hystrix的配置,否则feign配置的readTimeout不生效。
Feign中的Fallback降级配置不像hystrix中那样了。
1)首先,我们要定义一个类ProviderOpenfeignFallback,实现刚才编写的ProviderOpenfeign,作为fallback的处理类
@Component
public class ProviderOpenfeignFallback implements ProviderOpenfeign {
@Override
public User provider(String id) {
return "服务器繁忙,请稍后再试!";
}
}
2)然后在ProviderOpenfeign中,指定刚才编写的实现类
@FeignClient(value = "spring-provider",fallback = ProviderOpenfeignFallback.class) // 标注该类是一个feign接口
public interface ProviderOpenfeign {
@GetMapping("/provider/{id}")
String provider(@PathVariable("id") String id);
}
3)修改消费方controller的consumerOpenfeign方法,注释掉**@HystrixCommand注解和@DefaultProperties(defaultFallback=“fallback”)**进行测试。
@RestController
//@DefaultProperties(defaultFallback = "fallbackMethod")
public class ConsumerController {
@Autowired
private ProviderOpenfeign providerOpenfeign;
@RequestMapping(value = "/consumerOpenfeign/{id}")
//@HystrixCommand
public String consumerOpenfeign(@PathVariable String id){
String consumer = providerOpenfeign.provider(id);
return "consumerOpenfeign consumer " + consumer;
}
}
4)重启测试:
超时和超时重试
OpenFeign 本身也具备重试能力,在早期的 Spring Cloud 中,OpenFeign 使用的是 feign.Retryer.Default#Default() ,重试 5 次。但 OpenFeign 集成了Ribbon依赖和自动配置(默认也是轮询),Ribbon 也有重试的能力,此时,就可能会导致行为的混乱。(总重试次数 = OpenFeign 重试次数 x Ribbon 的重试次数,这是一个笛卡尔积。)
后来 Spring Cloud 意识到了此问题,因此做了改进(issues 467),将 OpenFeign 的重试改为 feign.Retryer#NEVER_RETRY ,即默认关闭。 Ribbon的重试机制默认配置为0,也就是默认是去除重试机制的,如果两者都开启重试,先执行ribbon重试,抛出异常之后再执行feign的重试。
所以,OpenFeign『对外』表现出的超时和重试的行为,实际上是 Ribbon 的超时和超时重试行为。我们在项目中进行的配置,也都是配置 Ribbon 的超时和超时重试
在调用方配置如下
# 全局配置
ribbon:
# 请求连接的超时时间
connectTimeout: 1000
# 请求处理的超时时间
readTimeout: 1000 #1秒
# 最大重试次数
MaxAutoRetries: 5
# 切换实例的重试次数
MaxAutoRetriesNextServer: 1
#NFLoadBalancerRuleClassName: RandomRule
# 对所有请求开启重试,并非只有get 请求才充实。一般不会开启这个功能。该参数和上面的3三个参数没有关系
#okToRetryOnAllOperations: true
feign:
hystrix:
enabled: true #默认是1s降级
#client:
#config:
# default:
# connectTimeout: 4000 #要关掉feign超时连接时长
#readTimeout: 150000
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 100000 #这个时间要大于ribbon重试次数的总时长,否则还没重试完就降级了
被调用方设置线程睡眠
@RestController
public class ProviderController {
@Value("${server.port}")
private String port;
@RequestMapping(value = "/provider/{id}")
public String provider(@PathVariable String id){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
return "exception:" + e.getMessage();
}
return "provider id = " + id + "port = " + port;
}
}
测试结果:由于openfeign重试默认是关闭的,我们不用管它。被调用方,如果只启动一个被调方实例,则一共12次,因为 MaxAutoRetriesNextServer: 1 切换下一个实例再重试,下一个实例还是自己,如果被调方启动两个实例,则各6次。另外重试和熔断都开启,超时时间是1s,一共是12次,也就是12s,12s之后就会降级,而hystrix配置的timeoutInMilliseconds的15s降级,在这里是以时间短的为主。如果不对timeoutInMilliseconds进行配置,那么hystrix默认是1s,也就是1s钟之后就会降级,但是不影响ribbon的重试。
你也可以指定对某个特定服务的超时和超时重试:
则其他的请求走上面的重试,spring-provider该服务的重试单独配置
# 针对 spring-provide 的设置,注意服务名是小写
spring-provide:
ribbon:
connectTimeout: 1000
readTimeout: 3000
MaxAutoRetries: 2
MaxAutoRetriesNextServer: 2
在被调方,修改如下代码测试
@RestController
public class ProviderController {
@Value("${server.port}")
private String port;
@RequestMapping(value = "/provider/{id}")
public String provider(@PathVariable String id){
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
return "exception:" + e.getMessage();
}
return "provider id = " + id + "port = " + port;
}
}
替换底层用HTTP实现
本质上是 OpenFeign 所使用的 RestTemplate 替换底层 HTTP 实现
- 替换成 HTTPClient
将 OpenFeign 的底层 HTTP 客户端替换成 HTTPClient 需要 2 步:
1、引入依赖:
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
2、在配置文件中启用它:
feign:
httpclient:
enabled: true # 激活 httpclient 的使用
- 替换成 OkHttp
将 OpenFeign 的底层 HTTP 客户端替换成 OkHttp 需要 2 步:
1、引入依赖
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2、配置
feign:
httpclient:
enabled: false # 关闭 httpclient 的使用
okhttp:
enabled: true # 激活 okhttp 的使用
日志级别(了解)
前面讲过,通过logging.level.xx=debug
来设置日志级别。然而这个对Fegin客户端而言不会产生效果。因为@FeignClient
注解修改的客户端在被代理时,都会创建一个新的Fegin.Logger实例。我们需要额外指定这个日志的级别才可以。然后根据 logging.level. 参数配置格式来开启 Feign 客户端的 DEBUG 日志,其中 部分为 Feign 客户端定义接口的完整路径。默认值是NONE,而NONE不会记录Feign调用过程的任何日志的,也就是说这个日志不是启动feign客户端的日志,而是feign调用远程接口时产生的日志。
1)设置com.woniu包下的日志级别都为debug
logging:
level:
com:
woniu:
openfeign:
ProviderOpenfeign: debug #ProviderOpenfeign为某个feign接口
2)编写配置类,定义日志级别
内容:
@Configuration
public class FeignLogConfiguration {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
这里指定的Level级别是FULL,Feign支持4种级别:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
3)在FeignClient中指定配置类:
@FeignClient(value = "spring-provider",fallback = ProviderOpenfeignFallback.class, configuration = FeignLogConfiguration.class)
public interface ProviderOpenfeign {
@GetMapping("/provider/{id}")
String provider(@PathVariable("id") String id);
}
4)重启项目,进行测试:在通过openfeign去调用某个接口时,会有详细的信息。如果把日志级别设置为NONE,则没有。
测试:http://localhost:8280/consumerOpenfeign
ml
logging:
level:
com:
woniu:
openfeign:
ProviderOpenfeign: debug #ProviderOpenfeign为某个feign接口
2)编写配置类,定义日志级别
内容:
```java
@Configuration
public class FeignLogConfiguration {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
```
这里指定的Level级别是FULL,Feign支持4种级别:
[外链图片转存中...(img-IP80Hi1k-1694490245931)]
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
3)在FeignClient中指定配置类:
```java
@FeignClient(value = "spring-provider",fallback = ProviderOpenfeignFallback.class, configuration = FeignLogConfiguration.class)
public interface ProviderOpenfeign {
@GetMapping("/provider/{id}")
String provider(@PathVariable("id") String id);
}
```
4)重启项目,进行测试:在通过openfeign去调用某个接口时,会有详细的信息。如果把日志级别设置为NONE,则没有。
测试:http://localhost:8280/consumerOpenfeign