发现问题
在写多服务互相调用的时候,发现远程feign调用方法正常情况下是无法将请求头的信息(例如token等)顺带传播的。
我们可以添加远程 feign 远程调用拦截器,来获取token 数据。
如上图:因为微服务之间并没有传递头文件,所以我们可以定义一个拦截器,每次微服务调用之前都先检查下头文件,将请求的头文件中的用户信息再放入到header中,再调用其他微服务即可。
package com.atguigu.tingshu.common.feign;import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;@Component
public class FeignInterceptor implements RequestInterceptor {public void apply(RequestTemplate requestTemplate){// 获取请求对象RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();if(null != requestAttributes) {ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes)requestAttributes;HttpServletRequest request = servletRequestAttributes.getRequest();String token = request.getHeader("token");requestTemplate.header("token", token);}}
}
我们再次清晰地梳理一下feign请求头丢失的问题:
原因:每次feign进行远程调用,会new一个新的Request,并发出去。不会看以前的老请求。
解决:利用拦截器;每次发请求之前。把老请求的请求头获取到放到RequestTemplate中,保证RequestTemplate有头信息就行。
PS:
同一个线程期间共享数据
- Map<Thread,数据>: 手动移除
- ThreadLocal<数据>: 可自动,推荐手动
- RequestContextHolder:
- 请求上下文。能获取到之前的老请求; 原理ThreadLocal机制,把用户信息隐式传参下去。
feign进行远程调用的源码分析
对feign整体流程的分析。
- 我们编写feignclient接口,SpringBoot容器启动会扫描每个接口并生成代理对象放在容器中
- 自动注入feign接口类,会拿到它的代理对象;
ReflectiveFeign.FeignInvocationHandler
1、SpringBoot启动扫描每个feign接口,并创建ReflectiveFeign
代理对象
2、并且把feign接口的每个方法,用什么处理器执行,提前保存起来
Map<Method, MethodHandler> dispatch;
当前方法对应的MethodHandler是 SynchronousMethodHandler
3、远程调用流程是在: SynchronousMethodHandler.invoke();
方法详细如下
public Object invoke(Object[] argv) throws Throwable {RequestTemplate template = buildTemplateFromArgs.create(argv);Options options = findOptions(argv);Retryer retryer = this.retryer.clone();while (true) {try {return executeAndDecode(template, options);} catch (RetryableException e) {try {retryer.continueOrPropagate(e);} catch (RetryableException th) {Throwable cause = th.getCause();if (propagationPolicy == UNWRAP && cause != null) {throw cause;} else {throw th;}}if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);}continue;}}}
详细流程:
-
1、创建请求模板; 封装准备要发送的请求的完整数据(注意观察,头里面没东西);新new的
-
2、发现参数位置是否有连接超时等设置参数。(我们没有,连接超时都是统一配置)
-
允许我们通过传参的方式精确调整每个请求的超时时间
-
//声明 @GetMapping("/add/{Id}/{num}")Result<SkuInfo> addToCart(@PathVariable("Id") Long id,@PathVariable("num") Integer num,Request.Options options);//调用 Request.Options options = new Request.Options(1000,2000); Result<Thing> result = testFeignClient.addThing(id, num,options);
-
-
3、获取重试器: 利用while(true),判断重试器如果抛了异常,while炸掉就不重试,重试器没抛异常,while就会continue
-
while (true) {try {//真正执行远程调用return executeAndDecode(template, options);} catch (RetryableException e) {try {retryer.continueOrPropagate(e);} catch (RetryableException th) {Throwable cause = th.getCause();if (propagationPolicy == UNWRAP && cause != null) {throw cause;} else {throw th;}}if (logLevel != Logger.Level.NONE) {logger.logRetry(metadata.configKey(), logLevel);}continue;}}
-
-
4、远程调用流程: executeAndDecode
feign是怎么进行远程调用
1、我们编写feign接口说清楚要调用哪个服务哪个地址@FeignClient
2、SpringBoot启动为每个接口创建代理对象
3、远程调用是代理对象在执行: 最终是 SynchronousMethodHandler
帮我们远程调用
4、先解析各种参数;构造出远程请求对象。(请求方式、请求路径、请求参数、请求头)
5、利用LoadBalancerFeignClient
负载均衡客户端先去nacos拿到对方微服务的所有地址
有缓存机制,如果以前调用过,其他被调用过的服务的 FeignLoadBalancer 会被缓存起来
6、根据这个地址构造出URL。然后openConnection();打开连接准备收发数据
7、用connection的outputstream把我们要发出去的数据写出去
8、用connection的inputstream感知对方给我们响应的数据
9、利用decoder把对方响应给我们的数据,逆转为方法的返回值类型