谷粒商城-高级-68 -商城业务-订单服务-Feign 远程调用丢失请求头问题

一、订单结算页

订单结算页Vo:
gulimall-order/xxx/order/vo/OrderConfirmVo.java

package com.atguigu.gulimall.order.vo;

import lombok.Getter;
import lombok.Setter;

import java.math.BigDecimal;
import java.util.List;
import java.util.Map;

/**
 * 订单确认页需要用到的数据
 *
 * @author: kaiyi
 * @create: 2020-09-14 00:38
 */
public class OrderConfirmVo {

  // 收货地址,ums_member_receive_address 表
  @Getter @Setter
  /** 会员收获地址列表 **/
  List<MemberAddressVo> address;

  @Getter @Setter
  /** 所有选中的购物项 **/
  List<OrderItemVo> items;

  /** 发票记录 **/
  @Getter @Setter
  /** 优惠券(会员积分) **/
  private Integer integration;

  /** 防止重复提交的令牌 **/
  @Getter @Setter
  private String orderToken;

  @Getter @Setter
  Map<Long,Boolean> stocks;

  public Integer getCount() {
    Integer count = 0;
    if (items != null && items.size() > 0) {
      for (OrderItemVo item : items) {
        count += item.getCount();
      }
    }
    return count;
  }

  /** 订单总额 **/
  //BigDecimal total;
  //计算订单总额
  public BigDecimal getTotal() {
    BigDecimal totalNum = BigDecimal.ZERO;
    if (items != null && items.size() > 0) {
      for (OrderItemVo item : items) {
        //计算当前商品的总价格
        BigDecimal itemPrice = item.getPrice().multiply(new BigDecimal(item.getCount().toString()));
        //再计算全部商品的总价格
        totalNum = totalNum.add(itemPrice);
      }
    }
    return totalNum;
  }

  /** 应付价格 **/
  //BigDecimal payPrice;
  public BigDecimal getPayPrice() {
    return getTotal();
  }

}

订单结算页控制器:
gulimall-order/xxx/order/web/OrderWebController.java

/**
 * @author: kaiyi
 * @create: 2020-09-14 09:43
 */
@Controller
public class OrderWebController {

  @Autowired
  OrderService orderService;

  @GetMapping("/toTrade")
  public String toTrade(Model model, HttpServletRequest request){

    OrderConfirmVo confirmVo = orderService.confirmOrder();

    model.addAttribute("confirmOrderData",confirmVo);
    //展示订单确认的数据

    // 展示订单确认的数据
    return "confirm";

  }

}

实现类:
gulimall-order/xxx/order/service/impl/OrderServiceImpl.java

  @Override
    public OrderConfirmVo confirmOrder() {

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

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

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

        // 2、远程获取购物车所有选中的的购物项
        List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
        confirmVo.setItems(currentCartItems);

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

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

        // TODO 5、防重令牌
        return confirmVo;
    }

展示结算页面:
http://order.gulimall.com/toTrade

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

在远程调用购物车会有问题,因为通过feign api接口请求购物车的微服务没有携带cookie信息,购物车校验没登录信息,直接返回空了,所以,获取不到购物车的信息。

file

要解决Feign调用丢失请求头的问题,加上Feign远程调用的请求拦截器。在Feign构建请求参数的时候,Feign拦截器会把增加的头信息参数添加进来。

创建Feign拦截器:
gulimall-order/xxx/order/config/GuliFeignConfig.java

package com.atguigu.gulimall.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;

/**
 * feign拦截器功能(在远程请求时携带认证信息cookie)
 *
 * @author: kaiyi
 * @create: 2020-09-14 14:26
 */
@Configuration
public class GuliFeignConfig {

  @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) {
          //老请求,携带了cookie
          HttpServletRequest request = requestAttributes.getRequest();

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

            // 给新请求同步了老请求的cookie
            template.header("Cookie", cookie);
          }
        }
      }
    };

    return requestInterceptor;
  }

}

添加了Feign拦截器之后,然后在 gulimall-cart 拦截器添加断点进行调试,可以看到,通过 gulimall-order 远程调用购物车服务,已经携带了cookie,购物车也可以根据cookie获取到登录的信息。

file

三、异步编排

在订单确认页,需要请求的数据比较多,该方法里边有两个远程调用,为了提高页面的响应速度,所以,可以考虑将确认订单的代码异步编排。

需要优化的代码:
gulimall-order/xxx/order/service/impl/OrderServiceImpl.java

  @Override
    public OrderConfirmVo confirmOrder() {

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

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

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

        // 2、远程获取购物车所有选中的的购物项
        List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
        confirmVo.setItems(currentCartItems);

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

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

        // TODO 5、防重令牌

        return confirmVo;

    }

订单微服务gulimall-order 引入线程池相关配置,可以参考我们gulimall-cart微服务的相关文章。

异步编排后的代码:

 @Override
    public OrderConfirmVo confirmOrder() throws ExecutionException, InterruptedException {

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

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

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

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

        }, threadPoolExecutor);

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

            // 2、远程获取购物车所有选中的的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);

            // feign在远程调用之前要构造请求,调用很多的拦截器
            // RequestInterceptor interceptor: requestInterceptors

        }, threadPoolExecutor);

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

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

        // TODO 5、防重令牌

        // 异步编排
        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;

    }

四、Feign异步情况调用丢失请求头的问题

通过上边的异步编排,我们在调试时发现了一个问题,即在异步模式下,Feign丢失了请求头,这是什么原因造成的呢?原因是在一连串调用(同步调用)时,从前到后都是同一条线程,所以,上下文、ThreadLocal 共享,如果使用异步,则新开了一个线程,而新开的线程没有相关的请求头信息。

file

上图说明:72表示orderService 主线程, 101表示异步获取address的线程,102表示异步获取cart的线程,这两个异步线程通过拦截器拿不到72号的主线程的共享ThreadLocal,在拦截器会报空指针异常。

为了解决异步请求头丢失的问题,我们可以在异步调用时,RequestContextHolder获取当前主线程的请求头信息,然后将住线程请求头信息放入到异步请求属性里边。

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

 //每一个线程都来共享之前的请求数据(设置属性代码需要放在异步任务代码片段里边)
RequestContextHolder.setRequestAttributes(requestAttributes);

完整代码:

 @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.setAddress(address);

        }, threadPoolExecutor);

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

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

            // 2、远程获取购物车所有选中的的购物项
            List<OrderItemVo> currentCartItems = cartFeignService.getCurrentCartItems();
            confirmVo.setItems(currentCartItems);

            // feign在远程调用之前要构造请求,调用很多的拦截器
            // RequestInterceptor interceptor: requestInterceptors

        }, threadPoolExecutor);

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

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

        // TODO 5、防重令牌

        // 异步编排
        CompletableFuture.allOf(addressFuture,cartInfoFuture).get();

        return confirmVo;

    }

为者常成,行者常至