feign拦截器解决头信息传递问题&异步编排头信息共享问题

一般情况下,我们会选择feign作为我们微服务之间的调用工具,但是往往也会带来一些问题。
如这种情况:
我们的订单服务需要从购物车进行结算,我们都知道,结算时必须是登陆状态,所以跳转后也应该是,但恰恰我们用了feign之后就会造成头信息丢失的问题,我们来看一个图就明白了。
在这里插入图片描述
由于feign的远程调用是新创建了一个request请求去执行,所以我们调用前的头信息就会丢失,所以我们需要使用feign拦截器在请求发出之前进行头信息的设置。

feign拦截器:

package com.xxx.xxx.order.config;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;

/**
 * @Description: feign拦截器功能
 * @Created: with IntelliJ IDEA.
 * @author: LY
 * @createTime: 2020-07-02 21:10
 **/

@Configuration
public class FeignConfig {
    
    
    @Bean("requestInterceptor")
    public RequestInterceptor requestInterceptor() {
    
    

        RequestInterceptor requestInterceptor = new RequestInterceptor() {
    
    
            @Override
            public void apply(RequestTemplate template) {
    
    
                //1、使用RequestContextHolder拿到刚进来的请求数据
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();

                if (requestAttributes != null) {
    
    
                    //老请求
                    HttpServletRequest request = requestAttributes.getRequest();

                    if (request != null) {
    
    
                        //2、同步请求头的数据(主要是cookie)
                        //把老请求的cookie值放到新请求上来,进行一个同步
                        String cookie = request.getHeader("Cookie");
                        template.header("Cookie", cookie);
                    }
                }
            }
        };
        return requestInterceptor;
    }
}

这样,我们使用feign调用之后的请求也会携带头信息了!

现在我们来优化一下调用,使用异步编排来进行调用,但是问题又来了,用了异步编排之后头信息再次丢失,什么情况?那就要说回来我们的登陆状态存储使用了 ThreadLocal 的原因了,我们来看一下当时我们存储登陆状态是怎么做的:

package com.xunqi.gulimall.order.interceptor;

import com.xunqi.common.vo.MemberResponseVo;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

import static com.xunqi.common.constant.AuthServerConstant.LOGIN_USER;

/**
 * @Description: 登录拦截器
 * @Created: with IntelliJ IDEA.
 * @author: LY
 * @createTime: 2020-07-02 18:37
 **/

@Component
public class LoginUserInterceptor implements HandlerInterceptor {
    
    

    //详解:https://www.cnblogs.com/fsmly/p/11020641.html
    public static ThreadLocal<MemberResponseVo> loginUser = new ThreadLocal<>();

    //在请求到达之前进行拦截
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    

        String uri = request.getRequestURI();
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        boolean match = antPathMatcher.match("/order/order/status/**", uri);
        boolean match1 = antPathMatcher.match("/payed/notify", uri);
        if (match || match1) {
    
    
            return true;
        }

        //获取登录的用户信息
        MemberResponseVo attribute = (MemberResponseVo) request.getSession().getAttribute(LOGIN_USER);

        if (attribute != null) {
    
    
            //把登录后用户的信息放在ThreadLocal里面进行保存
            loginUser.set(attribute);

            return true;
        } else {
    
    
            //未登录,返回登录页面
            response.setContentType("text/html;charset=UTF-8");
            PrintWriter out = response.getWriter();
            out.println("<script>alert('请先进行登录,再进行后续操作!');location.href='http://auth.gulimall.com/login.html'</script>");
            // session.setAttribute("msg", "请先进行登录");
            // response.sendRedirect("http://auth.gulimall.com/login.html");
            return false;
        }
    }

    //等待 preHandle 返回true 之后进行调用,且在视图渲染之前被调用
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
    
    

    }
    //等待 preHandle 返回true 之后进行调用,且在视图渲染之后被调用
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
    
    

    }
}

由于ThreadLocal具有线程隔离的特性,当我们使用异步编排时会造成头信息无法携带传递的问题,所以我们需要在每个异步线程里都重新把头信息赋值一次即可:
在这里插入图片描述
正确的调用代码:

    /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {
    
    

        //构建OrderConfirmVo
        OrderConfirmVo confirmVo = new OrderConfirmVo();

        //获取当前用户登录的信息
        MemberResponseVo memberResponseVo = LoginUserInterceptor.loginUser.get();

        //TODO :获取当前线程请求头信息(解决Feign异步调用丢失请求头问题)
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();

        //开启第一个异步任务
        CompletableFuture<Void> addressFuture = CompletableFuture.runAsync(() -> {
    
    

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //1、远程查询所有的收获地址列表
            List<MemberAddressVo> address = memberFeignService.getAddress(memberResponseVo.getId());
            confirmVo.setMemberAddressVos(address);
        }, threadPoolExecutor);

        //开启第二个异步任务
        CompletableFuture<Void> cartInfoFuture = CompletableFuture.runAsync(() -> {
    
    

            //每一个线程都来共享之前的请求数据
            RequestContextHolder.setRequestAttributes(requestAttributes);

            //2、远程查询购物车所有选中的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);
            //feign在远程调用之前要构造请求,调用很多的拦截器
        }, threadPoolExecutor).thenRunAsync(() -> {
    
    
            List<OrderItemVo> items = confirmVo.getItems();
            //获取全部商品的id
            List<Long> skuIds = items.stream()
                    .map((itemVo -> itemVo.getSkuId()))
                    .collect(Collectors.toList());

            //远程查询商品库存信息
            R skuHasStock = wmsFeignService.getSkuHasStock(skuIds);
            List<SkuStockVo> skuStockVos = skuHasStock.getData("data", new TypeReference<List<SkuStockVo>>() {
    
    });

            if (skuStockVos != null && skuStockVos.size() > 0) {
    
    
                //将skuStockVos集合转换为map
                Map<Long, Boolean> skuHasStockMap = skuStockVos.stream().collect(Collectors.toMap(SkuStockVo::getSkuId, SkuStockVo::getHasStock));
                confirmVo.setStocks(skuHasStockMap);
            }
        },threadPoolExecutor);

        //3、查询用户积分
        Integer integration = memberResponseVo.getIntegration();
        confirmVo.setIntegration(integration);

        //4、价格数据自动计算

        //TODO 5、防重令牌(防止表单重复提交)
        //为用户设置一个token,三十分钟过期时间(存在redis)
        String token = UUID.randomUUID().toString().replace("-", "");
        redisTemplate.opsForValue().set(USER_ORDER_TOKEN_PREFIX+memberResponseVo.getId(),token,30, TimeUnit.MINUTES);
        confirmVo.setOrderToken(token);


        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;
    }

我们在每个线程里面都再传递一遍我们的头信息,即可解决问题:

//每一个线程都来共享之前的请求数据
RequestContextHolder.setRequestAttributes(requestAttributes);

猜你喜欢

转载自blog.csdn.net/YL3126/article/details/120472960
今日推荐