Spring Cloud项目中通过Feign进行内部服务调用发生401\407错误无返回信息的问题

文章目录
  1. 1. 问题描述
  2. 2. 产生原因分析
    1. 2.1. 产生原因
    2. 2.2. 问题源代码示例
  3. 3. 解决思路
  4. 4. 源码解析
  5. 5. 解决方法
    1. 5.1. pom.xml文件中新增依赖
      1. 5.1.1. 替换为默认为okhttp Client
      2. 5.1.2. 替换为httpclient
  6. 6. 总结

前言 最近好几个小伙伴,问Spring Cloud项目中通过Feign进行内部服务调用发生401\407错误无返回信息的问题。这个问题如果没有自定义异常自定义Code或者系统中没有自定义code为401或407的code,基本很少能碰到。刚好Spring Cloud中国社区的VIP会员任聪博客原文也遇到这个,经过和他交流之后。整理出这篇文章希望能帮助更多的人快速定位问题。

问题描述

最近在使用Spring Cloud改造现有服务的工作中,在内部服务的调用方式上选择了Feign组件,由于服务与服务之间有权限控制,发现通过Feign来进行调用时如果发生了401、407错误时,调用方不能够取回被调用方返回的错误信息。

产生原因分析

产生原因

Feign默认使用java.net.HttpURLConnection进行通信,通过查看其子类sun.net.www.protocol.http.HttpURLConnection源码发现代码中在进行通信时单独对错误码为401\407的错误请求做了处理,当请求的错误码为401\407时,会关闭请求流,由于此时还并没有将返回的错误信息写入响应流中,所以接收的返回信息中仅仅能获取到response.status(),而response.body()为null。
HttpURLConnection相关信息的源码链接

问题源代码示例

1
2
3
4
5
6
7
if (respCode == HTTP_UNAUTHORIZED) {
if (streaming()) {
disconnectInternal();
throw new HttpRetryException (RETRY_MSG2, HTTP_UNAUTHORIZED);
}
//其余代码省略
}

java.net.HttpURLConnection中的HTTP_UNAUTHORIZED的定义如下:

1
public static final int HTTP_UNAUTHORIZED = 401;

解决思路

关于此问题产生的原因已经很明显了,就是feign.Client实现通信的方式选用了我们不想使用的HttpURLConnection。想到通常在Spring的代码中OCP都是运用得很好的,所以基本上有解决此问题的信心了,最不济就是自己扩展Feign,实现一个自己想要的feign.Client,当然这种事情Spring Cloud基本都会自己搞定,这也是Spring Cloud强大完善的一个地方。
通过这个思路查看源码,果然看到了Spring Cloud在使用Feign提前内置了三种通信方式(feign.Client.Default,feign.httpclient.ApacheHttpClient,feign.okhttp.OkHttpClient),其中缺省的情况使用的就是feign.Client.Default,这个就是使用HttpURLConnection通信的方式。

源码解析

在Spring Cloud项目中使用了Ribbon的组件,其会帮助我们管理使用Feign,查看org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
@Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignRibbonClientAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null),
cachingFactory, clientFactory);
}
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignLoadBalancedConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
ApacheHttpClient delegate;
if (this.httpClient != null) {
delegate = new ApacheHttpClient(this.httpClient);
}
else {
delegate = new ApacheHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
protected static class OkHttpFeignLoadBalancedConfiguration {
@Autowired(required = false)
private okhttp3.OkHttpClient okHttpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) {
OkHttpClient delegate;
if (this.okHttpClient != null) {
delegate = new OkHttpClient(this.okHttpClient);
}
else {
delegate = new OkHttpClient();
}
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
}
}
  • 从feignClient(CachingSpringLoadBalancerFactory cachingFactory, SpringClientFactory clientFactory) 方法结合其上注解我们可以很清楚的知道,当没有feign.ClientBean的时候会默认生成feign.Client.Default来进行通信,这就是之前说的缺省通信方式
  • 从HttpClientFeignLoadBalancedConfiguration、OkHttpFeignLoadBalancedConfiguration,我们可以看到其生效的条件,当classpath中有feign.httpclient.ApacheHttpClient并且配置feign.httpclient.enabled=true(缺省为true)、feign.okhttp.OkHttpClient并且配置feign.okhttp.enabled=true(缺省为true)
  • 当使用ApacheHttpClient或者OkHttpClient进行通信时就不会导致发生401\407错误时,取不到返回的错误信息了

解决方法

通过其上的分析,解决方法已经显而易见了替换默认的Client

pom.xml文件中新增依赖

替换为默认为okhttp Client

  1. 增加依赖
1
2
3
4
5
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-okhttp</artifactId>
<version>8.18.0</version>
</dependency>

2.在application.properties增加配置如下:

1
feign.okhttp.enabled=true

如何把默认的Client替换为okhttp在这里不做过多阐述,可以参考:spring cloud feign使用okhttp3

替换为httpclient

  1. 增加依赖
1
2
3
4
5
<dependency>
<groupId>com.netflix.feign</groupId>
<artifactId>feign-httpclient</artifactId>
<version>8.18.0</version>
</dependency>

2.在application.properties增加配置如下:

1
feign.httpclient.enabled=true

可以参考更换Feign默认使用的HTTP Client

总结

  • 由于新增的依赖没有被start管理,并且缺省不会导致程序启动异常,并且返回响应为null与此依赖没有直接关系,因此不方便定位到问题,特此记录下来,希望能帮助到遇到同样问题的人,如对文章有不同的看法,望给予指正。
  • 本文建立在已经搭建完成Feign的调用基础之上,没有讲述Feign的使用,因为此类文章很多,在此就不重复了,更多的信息可以参考如下文章。

快速使用Spring Cloud Feign作为客户端调用服务提供者
spring cloud feign使用okhttp3

如果您觉得文章不错,可以打赏我喝一杯咖啡!