一、 入门案例

1.1 引入依赖

  1. <dependency>
  2. <groupId>org.springframework.cloud</groupId>
  3. <artifactId>spring-cloud-starter-openfeign</artifactId>
  4. </dependency>

1.2 开启Feign客户端支持

在对应提供服务的项目启动类上使用此注解,开启feign支持。

  1. @EnableDiscoveryClient
  2. // 示例
  3. @SpringBootApplication
  4. @EnableDiscoveryClient
  5. public class GulimallCouponApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(GulimallCouponApplication.class, args);
  8. }
  9. }

1.3 声明调用接口

在需要远程调用的服务中,声明调用接口,使用注解@FeignClient指定被调用的服务名,接口方法的返回值及方法名入参都与被调用方法保持一致。同时,根据被调用的接口请求uri,拼接出远程调用的路径。
注意:

  1. - 远程调用路径包含了被调用接口的完整路径。
  2. - 请求方式保持一致。(`@PostMapping`等)
  3. - 入参注解保持一致。(`@RequestBody`等)

调用方:

@FeignClient("gulimall-coupon")
public interface CouponFeignService {
    @RequestMapping("/coupon/coupon/member/list")
    R memberCoupons();
}

被调用方:

@RestController
@RequestMapping("coupon/coupon")
public class CouponController {
    @Autowired
    private CouponService couponService;

    @RequestMapping("/member/list")
    public R memberCoupons() {
        CouponEntity couponEntity = new CouponEntity();
        couponEntity.setCouponName("满1块减50");
        return R.ok().put("coupons", Arrays.asList(couponEntity));
    }
}

1.4 调用

注入调用接口即可使用。

@RestController
@RequestMapping("member/member")
public class MemberController {
    @Autowired
    private MemberService memberService;

    @Autowired
    CouponFeignService couponFeignService;

    @RequestMapping("/coupons")
    public R test() {
        MemberEntity memberEntity = new MemberEntity();
        memberEntity.setNickname("小明");
                // 调用
        R memberCoupons = couponFeignService.memberCoupons();

        return R.ok().put("member", memberEntity).put("coupons", memberCoupons.get("coupons"));
    }
}

二、问题解决

2.1 丢失请求头问题

2.1.1 远程调用丢失请求头

在Feign远程调用时,通过动态代理生成feign接口的实现类。

通过以下判断逻辑,过滤掉不需要远程调用的方法,再进行远程调用。

public class ReflectiveFeign extends Feign {
    ...

        static class FeignInvocationHandler implements InvocationHandler {
            private final Target target;
            private final Map<Method, MethodHandler> dispatch;

            FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
              this.target = checkNotNull(target, "target");
              this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
            }

            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
              // 下面的方法就不需要远程调用
              if ("equals".equals(method.getName())) {
                try {
                  Object otherHandler =
                      args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
                  return equals(otherHandler);
                } catch (IllegalArgumentException e) {
                  return false;
                }
              } else if ("hashCode".equals(method.getName())) {
                return hashCode();
              } else if ("toString".equals(method.getName())) {
                return toString();
              }

              // 若不是上边的方法,则获取当前方法来执行
              return dispatch.get(method).invoke(args);
            }

            ...
            }
    ...
}

远程调用前,首先传入参数、创建对应的请求模板:

final class SynchronousMethodHandler implements MethodHandler {
    @Override
      public Object invoke(Object[] argv) throws Throwable {
        // 传入参数,创建请求模板
        RequestTemplate template = buildTemplateFromArgs.create(argv);
        Retryer retryer = this.retryer.clone();
        while (true) {
          try {
              // 获取模板,执行
            return executeAndDecode(template);
          } catch (RetryableException e) {
            try {
              retryer.continueOrPropagate(e);
            } catch (RetryableException th) {
              Throwable cause = th.getCause();
              if (propagationPolicy == UNWRAP && cause != null) {
                throw cause;
              } else {
                throw th;
              }
            }
            if (logLevel != Logger.Level.NONE) {
              logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
          }
        }
      }    
}

在下面的方法中,真正开始进行远程调用:

Object executeAndDecode(RequestTemplate template) throws Throwable {
    // 生成请求实体
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
        // 使用客户端发送请求
      response = client.execute(request, options);
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
      }
      throw errorExecuting(request, e);
    }

    ...
}

在生成请求实体的方法中,获取拦截器,遍历进行模板的完善填充:

Request targetRequest(RequestTemplate template) {
    // 遍历拦截器,修改模板
  for (RequestInterceptor interceptor : requestInterceptors) {
    interceptor.apply(template);
  }
  return target.apply(template);
}

远程调用时,请求丢失的原因分析:
feign调用的时候,创建了新的request对象,此对象中没有请求头在里边,所以丢失了前端携带过来的登录头信息。
image.png

我们可以通过构建feign内置的拦截器,来手动为创建的新request对象填充必要的登录头信息:
image.png

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

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

        return requestInterceptor;
    }

}

2.1.2 异步调用丢失请求头

在一个线程内部顺序执行多个远程调用逻辑的时候,可以通过拦截器在RequestContextHolder中正常的获取到当前线程内存储的请求属性信息。

为了提高效率,我们计划采用异步的方式同时远程调用去查询没有关联关系的数据:

    /**
     * 订单确认页返回需要用的数据
     * @return
     */
    @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.setMemberAddressVos(address);
        }, threadPoolExecutor);

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

            //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;
    }
<br />这时候就会遇到新的问题,在异步的远程调用时,发现请求头又丢失了,这是因为我们在创建了新的线程之后进行远程调用,这时候feign的拦截器是在当前线程中获取ThreadLocal中的请求属性信息,这会导致一个问题,就是新线程在自己的ThreadLocal中无法获取到之前线程中的数据,导致传递到远程接口中的请求头里没有对应的登录信息。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/22365594/1649769988063-171fa2a5-bcaf-4776-bb4d-4e1c7f426935.png#clientId=u46b132f1-4c1d-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=200&id=u804631a6&margin=%5Bobject%20Object%5D&name=image.png&originHeight=220&originWidth=1034&originalType=binary&ratio=1&rotation=0&showTitle=false&size=51125&status=done&style=none&taskId=ue6bd57c5-581d-493c-8162-61bdf7fdaeb&title=&width=939.9999796260491)

所以,我们需要在新开辟线程的时候,将老线程中的请求属性信息拷贝一份到新线程中,这样远程调用时就可以正确携带相关的登录数据抵达远程接口:
image.png

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

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

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

    //获取当前线程请求头信息(解决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;
}