0%

openFeign 远程调用丢失请求头问题

openFeign 远程调用

上图是 OpenFeign 远程调用的一个流程图,首先浏览器有一个向订单服务的请求1,由于用户已经登录此时该请求则默认带上了相关的 Cookie 信息。在订单服务中,其需要通过远程调用去请求购物车服务获取对应的数据,因而创建了对应的新的请求2。默认情况下该请求不会有任何 Cookie 等信息。因此在调用购物车服务时,由于购物车服务会判断用户是否已经登录从而去取请求的 Cookie ,因此此时请求2 的 Cookie 不存在从而购物车服务无法正常调用。

如下是订单服务的一个示例代码:

1
2
3
4
5
6
7
8
9
10
11
/**
* 获取订单确认信息
*/
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
// openfeign 远程调用购物车服务
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
return confirmVo;
}

购物车服务存在如下拦截器,其会拦截所有未登录用户的请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class CartInterceptor implements HandlerInterceptor {

public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从 session 中获取登录的用户信息,如果为空则不放行
HttpSession session = request.getSession();
MemberRespVo member = (MemberRespVo) session.getAttribute(AuthServerConstant.LOGIN_USER);
if (member == null) {
return false;
}
// ....
return true;
}
}

实际上我们通过代码调试时,confirmOrder 方法中第 8 行中的 cartFeignService 对象实际为一个代理对象,调用 getCurrentUserCartItems 方法时,其首先会调用 ReflectiveFeign 的 invoke 方法,如下图:

然后则是调用 SynchronousMethodHandler 的 invoke 方法:

接着会调用执行方法 executeAndDecode 方法,该方法首先会通过调用 targetRequest 获取请求信息:

targetRequest 方法中会遍历所有请求器链获取其次 openFeign 调用的所有请求信息:

image-20210718114048161

默认情况下 requestInterceptors 对象没有任何请求拦截器信息,因此 targetRequest 方法也就无法获取到任何请求信息。那么为了让 openFeign 的请求带上 Cookie 等信息,我们的解决办法就是向 Spring 容器中加入 RequestInterceptor,然后在该 RequestInterceptor 中封装我们的 Cookie 等信息即可。如下是订单服务的 RequestInterceptor 实现:

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
@Slf4j
@Configuration
public class FeignConfig {

/**
* 向容器中加入请求拦截器,并封装当前请求的 Cookie 信息
*/
@Bean("requestInterceptor")
public RequestInterceptor requestInterceptor() {
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
//1、RequestContextHolder拿到当前订单服务的请求
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
if (request != null) {
//同步请求头数据,Cookie
String cookie = request.getHeader("Cookie");
//给远程调用的新请求同步了当前请求的cookie
template.header("Cookie", cookie);
}
}
}
};
}
}

至此,我们便解决了 confirmOrder 方法在单线程远程调用购物车服务时的 openFeign 调用丢失请求头的问题。如下是相关示意图:

为什么这里要强调是在单线程情况下,下面我们改造 confirmOrder 方法,将其改造为多线程异步执行的执行方式。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
log.info("主线程...." + Thread.currentThread().getId());

CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//远程查询购物车所有选中的购物项
log.info("cart线程...." + Thread.currentThread().getId());
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);

CompletableFuture.allOf(cartFuture).get();
return confirmVo;
}

这时就会产生一个新的问题,调用订单服务的请求1所在的 ThreadLocal 包含有用户登录的信息,但是远程调用购物车服务的请求2 的 ThreadLocal 没有包含用户登录信息,因为两个请求不在同一个线程,请求2 在设置请求拦截器 RequestInterceptor 时使用的 RequestContextHolder 请求上下文是属于不同线程的,只有请求1 的 RequestContextHolder 才具备 Cookie 等信息,那么请求2 在设置 Cookie 时就会不起作用。为了解决可以问题,我们可以在 confirmOrder 方法中为请求2 手动设置上下文信息,那么请求2 就包含有对应的 Cookie 等信息,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Override
public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
OrderConfirmVo confirmVo = new OrderConfirmVo();
log.info("主线程...." + Thread.currentThread().getId());

//获取当前线程请求上下文,向子线程设置请求上下文
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
CompletableFuture<Void> cartFuture = CompletableFuture.runAsync(() -> {
//远程查询购物车所有选中的购物项
log.info("cart线程...." + Thread.currentThread().getId());
// 每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);
List<OrderItemVo> items = cartFeignService.getCurrentUserCartItems();
confirmVo.setItems(items);
}, executor);

CompletableFuture.allOf(cartFuture).get();
return confirmVo;
}

如下是单线程和多线程不同情况下的示意图:

------ 本文结束------