前言
事情的起因是因为我们在业务的日志中发现偶尔会出现一个这样的报错,而且正是因为这样的一个报错,导致我们一些用户的权益下发失败。
分析
看这个报错提示,能看出来是content-type请求时没带上。我们项目中使用的是openFeign进行微服务调用,那为什么会没有content-type呢? 查看代码观察到,对于的报错代码都使用了线程池进行异步业务处理,主线程则立即返回,线程池中线程调用时产生了如下的报错。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.5.RELEASE</version>
</dependency>
寻找解决途径
知道表象后我开始查阅百度谷歌资料…
直接github提个issue
issue地址 当时提了一个issue,那时候报错表象可能不一样,但究其原因是丢失了请求头content-type。但大佬们回答的效率确实不高,还建议我使用缺省的content-type。
本地搭建服务测试
发现服务能够正常的调用,但是当将feign异步时,主线程提前完成则失败,主线程等待则成功。
@RestController
public class TestController {
@Autowired
private AsyncFeignClient asyncFeignClient;
@PostMapping("/async")
public String async() throws InterruptedException {
Thread thread = new Thread(()->{
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
asyncFeignClient.postReq(new Model(),"123");
});
thread.start();
thread.join();
return "success";
}
}
@FeignClient(url = "127.0.0.1:9001",name = "async-feign",contextId = "identity")
public interface AsyncFeignClient {
@PostMapping(value = "/transport")
String postReq(@RequestBody Model mobile, @RequestHeader("token") String token);
}
去掉 thread.join() 服务端则会出现如下报错
源码debug
于是,借着这次机会再捋一下老熟人feignclient请求的流程。具体的流程太长,另起一篇记录。 如下是关键点
一开始进入feign调用后其实调用的是代理类的invoke方法,并且由于声明了请求头和@RequestBody所以template生成时有如下的头部 再往下走 executeAndDecode 方法 在这里targetRequest 之后头就被清空了,进去里面看到其实是自定义了拦截器导致
所以年轻人,自定义拦截器一定要小心。
也正是因为拦截器中的下面代码对template头部进行了清空 而究其原因就是在主线程结束后异步线程并没有上下问但他却取到了下面的request。 说来也奇怪为什么这里传空就不是追加而是清除还不是很理解。
到这里排查就结束了 ?
另外原因
其实这个问题还有一个原因,我的同事在误以为为什么无法传递头部的时候使用了如下代码
RequestContextHolder.setRequestAttributes(requestAttributes);
很可能参考了一些博客的做法
但人家是等待所有异步线程结束才返回,而这里的业务是直接返回,显然里面的头也就没有了也没有任何作用,并且导致了取到了空对象的上下文。 同时,也需要排查下框架中有无全局设置RequestContextHolder.setRequestAttributes(requestAttributes,true); 的地方,因为这个的作用等同于在异步线程设置requestAttributes 。
结束
所以看问题 早看源码早胜利 加油
|