你真的知道feign如何设置方法级别超时吗

开篇

通常情况下我们搭建微服务一般会选择SpringCloud或SpringCloudAlibaba,但也少不了其他组件、比如远程通信可以选择feign或dubbo。注册中心一般会选择Nacos或者Erurka等等, 本篇帖子以SpringCloud+feign+eureka为例,主要目的是为了解决feign调用超时的问题,但仅仅是feign的话可设置的超时来说还是有限的,它是针对于整个服务的,如果你要针对于服务下的某个接口设置超时,又该怎么做呢? 通过这篇帖子,我相信你的选择可能会更多

一:工程搭建

1:pom(客户端)
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-feign</artifactId>  
    <version>1.4.7.RELEASE</version>  
</dependency>  
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-sleuth</artifactId>  
    <version>1.3.6.RELEASE</version>  
</dependency>  
<dependency>  
    <groupId>org.springframework.cloud</groupId>  
    <artifactId>spring-cloud-starter-eureka</artifactId>  
    <version>1.4.7.RELEASE</version>  
</dependency>  

注:我使用的版本比较低,具体版本根据个人喜好来定

2:pom(服务端)
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-web</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-test</artifactId>  
<scope>test</scope>  
</dependency>  

注:服务端只是简单的springboot工程,并未应用其他依赖

3:yml配置(客户端)
server:  
    port: 8081  
  
eureka:  
    instance:  
        hostname: feignClient # eureka所在的服务器名  
    client:  
        fetch-registry: false  
        register-with-eureka: false  
        # eureka提供服务的地址  
        service-url:  
            defaultZone: http://localhost:9003/eureka
4:yml配置(服务端)
server:  
    port: 8082  
5:启动类(客户端)
@EnableFeignClients(basePackages = { "org.example.feign.*" })  
@SpringBootApplication  
public class SpringBootFeignClientApplication {  
public static void main(String[] args) {  
SpringApplication.run(SpringBootFeignClientApplication.class, args);  
}  
}  
6: 启动类(服务端)
@SpringBootApplication  
public class SpringBootFeignServerApplication {  
public static void main(String[] args) {  
SpringApplication.run(SpringBootFeignServerApplication.class, args);  
}  
}  

二:创建feign服务

1:客户端feign服务接口
@FeignClient(name = "vms-feign-client",  
url = "http://localhost:8082/feign/server/"  
)  
public interface FeignClientService {  
  
@RequestMapping(value = "/getData", method = RequestMethod.GET)  
String getData(@RequestParam("id") Integer id);  
}  

注:我这里的feign服务名是vms-feign-client,按个人喜好或根据业务定即可

2:具体实现
@RestController  
@RequestMapping("/feign/server")  
public class FeignServerController {  
  
@GetMapping("/getData")  
public String getData(Integer id) throws InterruptedException {  
Thread.sleep(8000L);  
System.out.println("请求到了");  
return "请求数据:id=" + id;  
}  
}  

项目结构

img_1.png

3:启动客户端、服务端,发送请求

img_4.png

img_3.png

三:配置超时

超时可分为两种一种是yml配置,一种是通过自定义配置类

让服务端的接口睡8秒钟

@GetMapping("/getData")  
public String getData(Integer id) throws InterruptedException {  
Thread.sleep(8000L);  
System.out.println("请求到了");  
return "请求数据:id="+id;  
}  

1、yml配置

feign:  
    client:  
        config:  
            vms-feign-client: # feign服务名  
                connect-timeout: 1000  
                read-timeout: 5000  

发起请求观察5秒钟之后如果没获取应答是否报错

发起请求 - Date :2024-01-19 14:29:50  
2024-01-19 14:29:55.280 ERROR [-,9171fa4af83f8de5,9171fa4af83f8de5,false] 21905 --- [nio-8081-exec-1] o.s.c.sleuth.instrument.web.TraceFilter : Uncaught exception thrown  
  
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is feign.RetryableException: Read timed out executing GET http://localhost:8082/feign/server/getData?id=13  
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982) ~[spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]  
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) ~[spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]  

2、自定义配置类

public class AccountFeignRequest {  
  
@Bean(name = "customOptions")  
public Request.Options feignRequestOptions() {  
return new Request.Options(1000,5000);  
}  
}  

修改feign服务,在@FeignClient注解中添加超时配置

@FeignClient(name = "vms-feign-client",  
url = "http://localhost:8082/feign/server/",  
configuration = AccountFeignRequest.class  
)  
public interface FeignClientService {  
  
@RequestMapping(value = "/getData", method = RequestMethod.GET)  
String getData(@RequestParam("id") Integer id);  
  
}  

再次发起请求

发起请求 - Date :2024-01-19 14:48:03  
2024-01-19 14:48:08.619 ERROR [-,1ee9c2d20f32f312,1ee9c2d20f32f312,false] 22177 --- [nio-8081-exec-1] o.s.c.sleuth.instrument.web.TraceFilter : Uncaught exception thrown  
  
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is feign.RetryableException: Read timed out executing GET http://localhost:8082/feign/server/getData?id=13  
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:982) ~[spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]  
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:861) ~[spring-webmvc-4.3.13.RELEASE.jar:4.3.13.RELEASE]  
  

四:方法级别配置超时

刚开始觉得这个很简单,但是看了下feign源码,貌似只能针对于服务,没办法针对服务下的某个方法,显然
粒度还是太大,总不能写多个服务吧,那这样的话太不灵活了。某度某金等博客网站搜索一番有几个说能实现的,但实际
也不能实现,所以决定自己翻一番源码看能不能找到灵感,没想到确实还有扩展的地方

灵感

我的想法是服务级别可以通过Options配置超时,而且每次调用的时候接口都是按服务级别配置的,于是我想看一下读取Options这块的逻辑是怎么样的,是不是将它组装到了方法上,这一看不得了,果然我的想法得到了验证,请求发送时会执行execute方法,在execute方法的形参中上游将Options对象传递过来了,于是我觉得可以在这个地方动手脚,源码如下

img_5.png

??????源码重写

我发现这个方法是在Default类的,而Default是Client的实现,继续往上找这个类被加载到了spring的容器中,代码如下

package org.springframework.cloud.netflix.feign.ribbon;  
  
import feign.Client;  
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;  
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;  
import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
  
/**  
* @author Spencer Gibb  
*/  
@Configuration  
class DefaultFeignLoadBalancedConfiguration {  
@Bean  
@ConditionalOnMissingBean  
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,  
SpringClientFactory clientFactory) {  
return new LoadBalancerFeignClient(new Client.Default(null, null),  
cachingFactory, clientFactory);  
}  
}  

看到这就算是找到了可以重写的地方了,说干就干

创建Client自定义实现

public class CustomDefault extends Client.Default implements Client {  
  
public CustomDefault(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) {  
super(sslContextFactory, hostnameVerifier);  
}  
  
@Override  
public Response execute(Request request, Request.Options options) throws IOException {  
Map<String, Collection<String>> headers = request.headers();  
Collection<String> readTimeout = headers.get("read-timeout");  
Collection<String> connectTimeout = headers.get("connect-timeout");  
Request.Options customOptions = null;  
if (!CollectionUtils.isEmpty(readTimeout)  
&& !CollectionUtils.isEmpty(connectTimeout)) {  
customOptions = new Request.Options(  
Integer.parseInt(connectTimeout.iterator().next()),  
Integer.parseInt(readTimeout.iterator().next()));  
}  
return super.execute(request, customOptions == null ? options : customOptions);  
}  
}  

重写的execute方法中的逻辑为什么要获取请求头呢

Collection<String> readTimeout = headers.get("read-timeout");  
Collection<String> connectTimeout = headers.get("connect-timeout");  

原因是因为我发现debug跑到execute方法时,request实际上就是@RequestMapping的参数转换,它把url、参数、请求头和Method等条件转换成了Request对象,我想的是如果要控制方法级别的超时不可能对请求参数做改动,所以就想到了再@RequestMapping中添加请求头的方式来设置标识,这样依赖的话,上面的逻辑就可以说的通了。
意思就是如果请求中没有我想要的请求头那么就按照默认的超时时间进行相应,如果有的话,则按照方法上的请求超时时间设置,修改后代码如下:

@RequestMapping(value = "/getData", method = RequestMethod.GET,headers = {"connect-timeout=1000","read-timeout=3000"})  
String getData(@RequestParam(value = "id") Integer id);  
将这个自定义的实现交给spring进行托管
@Configuration  
public class CustomFeignLoadBalancedConfiguration {  
  
@Bean  
@Primary  
@ConditionalOnMissingBean  
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,  
SpringClientFactory clientFactory) {  
return new LoadBalancerFeignClient(new CustomDefault(null, null),  
cachingFactory, clientFactory);  
}  
}  
测试

服务的超时设置还是5秒钟

public class AccountFeignRequest {  
  
@Bean(name = "customOptions")  
public Request.Options feignRequestOptions() {  
return new Request.Options(1000,5000);  
}  
}  

请求头超时时间设置3秒

@FeignClient(name = "vms-feign-client",  
url = "http://localhost:8082/feign/server/",  
configuration = AccountFeignRequest.class  
)  
public interface FeignClientService {  
  
@RequestMapping(value = "/getData", method = RequestMethod.GET,headers = {"connect-timeout=1000","read-timeout=3000"})  
String getData(@RequestParam("id") Integer id);  
  
}  

发起请求:

img_6.png

自定义的execute方法执行了,现在options的时间是自定义超时配置中设置的超时,当经过自定义的execute逻辑后,会再次出发父类中的execute方法,观察父类中的时间是否被修改成3秒:
img_7.png
成功的来了一波偷天换日,如果需要对服务下不同接口设置不同的超时,我觉的这篇文章会帮到你,点个赞吧,下一篇文章继续讲第二种方式设置超时时间