一止长渊

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

N 人看过
字数:1.6k字 | 预计阅读时长:6分钟

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

在订单模块 Feign 调用购物车模块,查询用户的购物车详情时,购物车拦截器是通过拦截 SpringSession 来获取用户的信息的,通过订单模块和购物车模块直接从浏览器访问页面是可以直接从 SpringSession 中获取用户信息的,但是在 Fegin 相互调用经过购物车的拦截器时无法获取到用户的信息,这就是 Feign 远程调用丢失请求头的问题。
截屏2021-06-15 16.09.27.png

症结:SpringSession 获取用户状态,本质上是通过浏览器端携带的 jsessionid 然后通过 redis 查询用户的信息的,但是在 feign 微服务模块相互调用之间,订单模块向购物车模块发出请求时,没有向浏览器端访问那样在请求头中携带请求头 jsessionid,因此购物车的模块从 SpringSession 中无法获得用户的信息,因此无法得知用户的购物车详情。
浏览器访问订单模块时,发出的请求是自动携带上了 cookie jsessionid,但是订单模块在向购物车模块 feign 远程调用发出的请求是构造了一个新的请求 Request,没有任何请求头
截屏2021-05-20 23.27.36.png截屏2021-05-20 23.28.01.png

可以从 Feign 源码看到相互调用时是构造了一个新的请求 Request,这个请求在构造过程中会通过很多拦截器对请求进行增强,interceptor.apply(template)

解决:
为此请求头的问题,我们可以通过加入 feign 远程调用的请求拦截器,在 Feign 调用时通过拦截器向请求头中添加 cookie
Feign 调用时创建一个新的请求,如何获取之前浏览器端发送请求中的 Cookie 呢?

接下来如何获得浏览器访问请求的请求头加入到构造的请求中,Controller 接收的请求交给 Service,Service 调用 Fegin 都是同一个线程进行调用,此外也可以通过 Spring 提供的RequestContextHolder来获得当前请求的上下文环境,本质上RequestContextHolder 是通过 ThreadLocal 拿到当时浏览器发送的请求,因为是同一个线程调用,所以可以获得当前线程中第一个浏览器发送的请求。

@Configuration
public class DoermallFeignConfig {
    @Bean
    public RequestInterceptor requestInterceptor(){
        return new RequestInterceptor() {
            @Override
            public void apply(RequestTemplate requestTemplate) {
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
                HttpServletRequest request = requestAttributes.getRequest();  // 老请求:浏览器发出的请求
                // 同步请求头数据,Cookie
                String cookie = request.getHeader("Cookie");
                requestTemplate.header("Cookie", cookie);
            }
        };
    }
}

Feign 异步丢失上下文环境问题

接下来解决了 Feign 丢失请求头的问题,为了保证订单确认模块的高效,使用了异步发送请求,分别向会员模块查询用户的收货地址,以及向购物车模块查询用户的确认购物项。**

java.lang.NullPointerException: null
    at com.lookstarry.doermail.order.config.DoermallFeignConfig$1.apply(DoermallFeignConfig.java:27) ~[classes/:na]
    at feign.SynchronousMethodHandler.targetRequest(SynchronousMethodHandler.java:169) ~[feign-core-10.2.3.jar:na]
    at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:99) ~[feign-core-10.2.3.jar:na]
    at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:78) ~[feign-core-10.2.3.jar:na]
    at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:103) ~[feign-core-10.2.3.jar:na]
    at com.sun.proxy.$Proxy103.getAddressById(Unknown Source) ~[na:na]
    at com.lookstarry.doermail.order.service.impl.OrderServiceImpl.lambda$confirmOrder$0(OrderServiceImpl.java:63) ~[classes/:na]
    at java.util.concurrent.CompletableFuture$AsyncRun.run$$$capture(CompletableFuture.java:1640) ~[na:1.8.0_251]
    at java.util.concurrent.CompletableFuture$AsyncRun.run(CompletableFuture.java) ~[na:1.8.0_251]
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) ~[na:1.8.0_251]
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) ~[na:1.8.0_251]
    at java.lang.Thread.run(Thread.java:748) [na:1.8.0_251]

截屏2021-06-15 16.09.49.png
分析:RequestContextHolder 是将上下文环境 ThreadLocal 中的,如果是一条线程从头至终的话,是可以获取 ThreadLocal 中存放的 Cookie 的;但是这里是异步的情况,当发送 Fegin 的时候,我们为了提高速度,使用了线程池异步发送,相当于单开了一条线程,所以单开的 Fegin 线程与浏览器发送请求的线程不再是同一条线程了,无法再从 ThreadLocal 中拿到原浏览器线程中的上下文环境
PubZiCAbstRAcTCLAsREqUESTConTextHoder
privatestaticfinal
omrngemt.u
TRedolRett
privatestaticfinal
ewNamedInheritableThreadLocaRequestcontext”
final
THREADLOCLUestAttutLRqetutes
privatestatic
截屏2021-06-15 15.38.43.png

添加打印线程信息:

    /**
     * 订单确认页面信息:用户的收货地址信息、结算商品信息、
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        MemberInfoEntity loginUser = LoginUserInterceptor.threadLocal.get();
        System.out.println("主线程..." + Thread.currentThread().getId());

        // 1、查询所有的收货地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
            System.out.println("member线程..." + Thread.currentThread().getId());
            List<MemberAddressVo> addresses = memberFeignService.getAddressById(loginUser.getId());
            orderConfirmVo.setAddress(addresses);
        }, executor);


        // 2、远程查询用户购物车中所有选中的购物项
        CompletableFuture<Void> getCartItemsFuture = CompletableFuture.runAsync(() -> {
            System.out.println("购物车线程..." + Thread.currentThread().getId());
            List<OrderItemVo> items = cartFeignService.currentUserCartItems();
            orderConfirmVo.setOrderItems(items);
            // feign在远程调用之前要构造请求,调用很多的拦截器
            // RequestInterceptor
        }, executor);

        // 3、查询用户积分信息
        Integer integration = loginUser.getIntegration();
        orderConfirmVo.setIntegration(integration);

        // 4、订单总额、应付总额自动由get方法计算

        CompletableFuture<Void> allFuture = CompletableFuture.allOf(getAddressFuture, getCartItemsFuture);
        allFuture.get();

        // TODO 幂等性 5、订单防重复下单令牌
        return orderConfirmVo;

    }

可以看到两次调用 Fegin 线程,与主线程号不同
截屏2021-06-15 15.51.43.png

解决:
在调用异步线程前将原线程的上下文环境手动放入到异步线程中

    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
        OrderConfirmVo orderConfirmVo = new OrderConfirmVo();
        MemberInfoEntity loginUser = LoginUserInterceptor.threadLocal.get();
//        System.out.println("主线程..." + Thread.currentThread().getId());
        // 主线程获取上下文环境
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        // 1、查询所有的收货地址列表
        CompletableFuture<Void> getAddressFuture = CompletableFuture.runAsync(() -> {
//            System.out.println("member线程..." + Thread.currentThread().getId());
            // 子线程主动塞入父线程上下文环境
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<MemberAddressVo> addresses = memberFeignService.getAddressById(loginUser.getId());
            orderConfirmVo.setAddress(addresses);
        }, executor);


        // 2、远程查询用户购物车中所有选中的购物项
        CompletableFuture<Void> getCartItemsFuture = CompletableFuture.runAsync(() -> {
//            System.out.println("购物车线程..." + Thread.currentThread().getId());
            RequestContextHolder.setRequestAttributes(requestAttributes);
            List<OrderItemVo> items = cartFeignService.currentUserCartItems();
            orderConfirmVo.setOrderItems(items);
            // feign在远程调用之前要构造请求,调用很多的拦截器
            // RequestInterceptor
        }, executor);

        // 3、查询用户积分信息
        Integer integration = loginUser.getIntegration();
        orderConfirmVo.setIntegration(integration);

        // 4、订单总额、应付总额自动由get方法计算

        CompletableFuture<Void> allFuture = CompletableFuture.allOf(getAddressFuture, getCartItemsFuture);
        allFuture.get();

        // TODO 幂等性 5、订单防重复下单令牌
        return orderConfirmVo;

    }

本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 (CC BY-NC-ND 4.0) 进行许可。