上图是 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(); 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 { 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 调用的所有请求信息:
默认情况下 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 {
@Bean("requestInterceptor") public RequestInterceptor requestInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest request = attributes.getRequest(); if (request != null) { String cookie = request.getHeader("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; }
|
如下是单线程和多线程不同情况下的示意图: